How the Runtime Works
This page traces one run from the event that starts it to the record it leaves behind. If you're debugging behaviour, deciding where to put logic, or asking "where would I see that?" — this is the map.
The shape of it
signal ──► match agent ──► resolve model ──► pre-run policy
│
execution tier
│
┌─────────────┴─────────────┐
│ the agent's tool loop │
│ tools · domains · LLM │
└─────────────┬─────────────┘
│
mid-run policy on every tool call
│
post-run policy ──► spans · cost · audit
Everything after "match agent" is identical no matter which execution tier the run lands on. That's the point of the tier split: where a run executes is an infrastructure decision; what governs it is not.
1. A signal arrives
A run starts when something calls one of your agent's declared signals:
signals:
- name: document_received
source_type: api
schema:
document_id: string
idempotency_key: $.document_id
wax -p prod signals fire document_received --payload '{"document_id":"abc123"}'
The platform matches the signal name to the agent that declared it, creates an
AgentExecutionRun row in pending, and returns immediately. The caller does
not wait for the agent. Your API stays fast; the run proceeds in the
background.
idempotency_key is a JSONPath into the payload. Two signals carrying the same
key collapse to one run — the guard against a webhook delivered twice.
2. The model is resolved
Waxell resolves the model in three layers, highest priority first:
- Per-run override — passed explicitly on the call
- Tenant routing — your configured provider instances and task routes
- Declared — the
model:in yourwaxell.yaml
This is why the same agent can run on different models in dev and prod without a code change, and why a tenant can move providers without touching an agent.
The resolved provider instance is recorded on the run, so cost attributes to the right provider even when routing changes mid-month.
3. Governance runs before the agent does
Policies evaluate before the first token is generated. A blocking policy stops the run outright — no LLM call, no cost.
policies:
- block_pii_leakage
- require_human_review
Those names reference policies in your tenant's policy engine, where the actual rules, scope, and action live. Policies scoped to an agent apply whether or not the agent names them — see Adding Governance for the code-first path and all 49 categories.
There are three evaluation points, and they're different on purpose:
| When | Sees | Typical use |
|---|---|---|
| Before | Inputs, agent identity, caller | Block disallowed inputs; require approval up front |
| Mid-run | Each tool call, before it executes | Stop a specific action — a write, a spend, an external send |
| After | The full output and span tree | Redact, flag for review, raise an incident |
Mid-run is the one people underestimate. An agent that decided to do something disallowed is stopped at the call, not after the fact.
A blocked run is a first-class outcome: it finishes with a recorded decision and
an ExecutionIncident, not an exception.
4. The agent executes
The runtime hands the agent its prompt, its tools, and an execution context.
Tools come from three places, and they arrive as one list to the model:
tools:— your own registered toolsdomains:— callbacks into your application (see Domains)- Native runtime tools — spawn, wait, ask-user, memory
Domains are the important one for most teams. Rather than giving an agent database credentials, you expose named actions:
domains: [document, deal]
The agent calls document.read(...); your service answers over a shared-secret
callback. Your data stays in your application, the agent gets an interface, and
every call is a governed, traced step.
The loop runs until the agent stops calling tools, hits timeout_seconds, or a
policy stops it.
5. Where it ran
The tier is chosen per agent — see Execution Tiers. Shared workers by default; a warm slot for per-execution isolation; a self-contained container when the agent needs its own dependencies.
The run records which tier it used, and that decides the compute multiplier on your bill. Nothing about governance or telemetry changes with the tier.
6. What the run leaves behind
Every run produces the same record regardless of tier:
| Artifact | What it holds | Where you see it |
|---|---|---|
| Span tree | Every LLM call, tool call, and domain call, nested, timed, with errors marked | Observe → Runs → trace |
LlmCallRecord | Model, tokens in/out, cost, provider instance | Observe, and Billing → Usage |
| Policy decisions | What evaluated, what it decided, why | Observe → Governance |
ExecutionIncident | Errors, policy blocks, budget stops | Observe → Errors |
| Usage events | Agent execution, CPU-seconds, memory, LLM tokens | Billing → Usage |
| Audit log | Who/what triggered it, under which identity | Audit |
A failed run is as fully recorded as a successful one. If a run "did nothing", the trace shows you why — no tool calls, a policy block, or an empty response.
Where to put logic
The most common design question, and the one that costs the most to get wrong:
| Put it in… | When |
|---|---|
| The system prompt | Judgement. Classification, extraction, tone, "which of these six is it". |
| A domain action | Anything deterministic or stateful. Writing records, computing totals, deciding state transitions. |
| A policy | Rules that must hold regardless of what the agent decides. |
| A guard/limit | Bounds: cost, turns, time. |
The rule of thumb: if being wrong is expensive, it doesn't belong in the prompt. An LLM should decide which invoice this is; it should not be the thing that decides whether two invoices are the same record. Put the judgement in the agent and the arithmetic in your domain.
Failure behaviour
Worth knowing before you rely on it:
- The agent returns nothing useful. The run completes; the span tree shows an empty tool loop. Usually a prompt problem, and the trace tells you which.
- A tool or domain call fails. The error is recorded on that span and surfaced as an incident. The agent sees the failure and may retry or route around it.
- Timeout.
timeout_secondscaps wall time; the run is marked failed with what completed preserved. - A policy blocks. Recorded as a decision with its reason. Not an error.
- Infrastructure dies mid-run (OOM, host loss). The run is finalised as failed by a reconciler rather than sitting unfinished.
Related
- Adding Governance — limits, approvals, and the policy engine
- Execution Tiers — where runs execute, and what it costs
- Domains — how agents call back into your application
- Execution Context — what the agent can access at runtime
waxell.yamlreference — every supported field