Skip to main content

Execution Context

Every decorated method receives ctx as its second argument, after self. It's how your agent reaches everything outside itself: your application, the LLM, tools, memory, secrets — and how it pauses for a human.

from waxell_runtime import agent, workflow


@agent(name="report_builder", description="Builds a report")
class ReportBuilder:

@workflow("build")
async def build(self, ctx, quarter: str):
data = await ctx.domain("finance", "quarterly", quarter=quarter)
return await ctx.llm.generate(prompt=f"Summarize: {data}")
Everything that reaches outside the agent is async

ctx.domain, ctx.tool, ctx.llm.generate, ctx.spawn, ctx.ask_user and friends are coroutines. Forget the await and you get a coroutine object instead of your result — Python only warns, so it fails quietly.

For editor autocomplete, type-hint it:

from waxell_runtime import WorkflowContext, ToolContext

@workflow("build")
async def build(self, ctx: WorkflowContext, quarter: str): ...

@tool
async def fetch(self, ctx: ToolContext, id: str) -> dict: ...

Reaching your application

ctx.domain() is the main one. It calls a named action on a domain you've registered; your service answers over a shared-secret callback — so the agent never holds your credentials.

company = await ctx.domain("company", "get", company_id="acme")

See Domains.

Calling a tool

ctx.tool() invokes another @tool on this agent. Each call is a durable checkpoint — see Durable Execution.

data = await ctx.tool("fetch_company", domain="acme.com")

The LLM

Routed, cost-attributed and policy-checked. You never hold a provider key.

summary = await ctx.llm.generate(
prompt="Summarize this contract",
task="contract_summary", # names the task for routing + cost analytics
max_tokens=2000,
)

Secrets

key = ctx.secrets.get("STRIPE_API_KEY")

Resolved from the tenant's configured secret provider. Don't read os.environ — it works locally and fails in deployment.

Memory

await ctx.memory.set("last_seen", value)
value = await ctx.memory.get("last_seen")

ctx.scratchpad is the short-lived working tier for a single run. See Working Memory.


Pausing for something external

These suspend the run without holding compute. It resumes when the thing arrives.

answer = await ctx.ask_user("Approve this $40k discount?")
event = await ctx.wait_for_signal("po_received", timeout_seconds=86400)
await ctx.sleep(3600)

A paused run shows as PAUSED with a reason — see Durable Execution.

Fanning out

child   = await ctx.spawn("enrichment_agent", inputs={"lead_id": lead_id})
results = await ctx.spawn_many("scorer", [{"id": i} for i in ids])

Children are governed and traced like any other run, and appear under the parent in the lineage graph.


Identity and provenance

AttributeWhat it is
ctx.inputsThe inputs this run was started with
ctx.workflow_idThis run's id
ctx.parent_workflow_idThe run that spawned this one, if any
ctx.root_workflow_idThe top of the spawn tree
ctx.metadataFree-form metadata carried with the run
ctx.sub_user_id / ctx.sub_user_emailThe end user this run is acting as, when it carries a signed sub-user identity

sub_user_* is what makes per-end-user auth work: the agent acts as a specific person, and your domain endpoint can verify that cryptographically.

Observability

ctx.log_step("validated_input", {"rows": len(rows)})   # note: not async
await ctx.checkpoint() # force a save point

ctx.budget exposes the run's spend against its cap.