The four step kinds
Every step has an execution field that says who performs it. There are four values: auto, agent, guard and finalizer. This page explains what each kind is for, what it leaves in the record, and how to choose between them.
| Kind | Who performs it | Use it for |
|---|---|---|
agent |
Whoever drives the run | Work that needs judgement: a model, or a person. |
auto |
The engine | Work that code can do: call a service, run a function. |
guard |
The engine | A rule that must hold, or the run stops. |
finalizer |
The engine | Cleanup that runs when the run ends. |
The examples on this page come from one small workflow that pays a refund. The whole file is on Order and routing.
Agent steps
Section titled “Agent steps”An agent step waits for an answer from outside the engine. The engine hands out the step’s description and its input_schema, and accepts the answer only if it fits the schema.
assess: description: Decide how risky the refund is and state the amount. execution: agent input_schema: type: object additionalProperties: false required: [risk, amount] properties: risk: type: string enum: [low, high] amount: type: numberWho answers depends on how the run is driven. It can be an AI assistant connected over MCP, a model called by realm agent, or you at the terminal. The engine treats all three the same way. See Who drives a run.
Auto steps
Section titled “Auto steps”The engine runs an auto step itself. No model is involved. An auto step does one of three things:
- It calls a handler, a function from your project, named with
handler:. - It calls an outside service through an adapter, named with
uses_service:. - It does nothing of its own. A step with neither records an output it did not compute: when the engine runs it, the recorded output of its one
depends_onstep (with nodepends_on, the run’s params; with several,{}); when a caller names it, what the caller’s dispatcher returned. Its evidence says which (output_source). This is useful as a place to put a human gate. Added after version 0.46.0, which records what it received.
An auto step the run’s view refuses before it is claimed — an invalid trust, a failed precondition, or an input its schema rejects — is never submitted, so it never fails; it cannot run anywhere until the run or the workflow changes. A step whose handler or adapter this program has not registered is different: it cannot run here, and a program that has it can run it. Either way the step is named: get_run_state lists it under engine_runnable with the check that refused it (an input refusal names the field and what it must be, or the property the schema does not allow: '<property>' is not allowed), realm run inspect prints Cannot run '<step>' (<check>): <why>, and realm agent prints • Step '<step>' cannot run (<check>): <why> — cannot run here (capability) for a missing handler or adapter, ending — load the missing extension, or run the step on a runner that has it — once, and goes on with any agent step that is ready. An invalid trust is named in the words the run-health finding uses (the engine will refuse this step at dispatch (VALIDATION_TRUST_VALUE)): nothing has been dispatched. The advance_run act stops naming it.
An agent step is judged the same way for an invalid trust and a failed precondition, before any model is asked to answer it (its input schema is not judged: its input is the answer). A refused agent step is never offered: get_run_state lists it under agent_refused, every reply that says what comes next names it with the same '<step>' cannot run (<check>): <why>. words instead of Ready for the agent, realm run inspect prints Cannot run '<step>' (<check>): <why>, and realm agent names it once and never calls the model for it. Added after version 0.46.0: version 0.46.0 offers the step, and realm agent calls the model for it again and again.
When nothing else can run, realm agent stops on the first such step in the workflow’s order and exits 1. For a refusal before the claim it prints one line, ✗ The drive stops: nothing else can run, and '<step>' cannot run (<check>). Run <id> stays open (phase '<phase>'): correct the workflow, register it again, then realm run advance <id> — or end it: realm run abandon <id> When a step that cannot run is refused for its input, the line gives each such step’s own way out instead, in the workflow’s order, after stays open (phase '<phase>'): — for '<step>', correct the workflow and register it again for a refused trust or precondition, for '<step>', start a run with params that fit, or correct its input_schema and register the workflow again for an input refused with no depends_on (the engine gives such a step the run’s params), and for '<step>', the engine gives it no input, so correct its input_schema and register the workflow again for one with depends_on; for a step that builds its input with an input_map (one with a handler or a uses_service), its schema checks what the map builds, and the way is for '<step>', correct its input_map or its input_schema and register the workflow again, with start a run with params that fit, or before correct when the key its schema refuses is built from run.params — joined by ; , then ; then, after a fix, realm run advance <id> — or end it: realm run abandon <id> (added after version 0.46.0). Correcting the workflow and registering it again is the fix: the run picks up the corrected definition, and realm run advance then runs the step when it is an auto step; for an agent step it names the drive (Nothing is owed to the engine: an agent step is ready: '<step>' — drive it with realm agent --run-id <id> --provider <provider> --model <model>). An input its schema rejects is also recorded as a drive failure, so realm run list --stuck names the step. For a missing handler or adapter, each drive that has nothing else to run attempts the step once and records each attempt in capability_blocks, so the run says which one is missing; the drive then prints the block’s ⚠ … re-attach line.
A program judges a missing handler or adapter with its own extensions: realm agent, realm run advance, and realm mcp or realm serve — a server with no project extensions has an empty set, so it says handler '<name>' is not registered here. Only a caller that passes no extensions at all — a program that embeds the engine, or realm run inspect — judges by the run’s record, in the past tense: could not run (capability): handler '<name>' was not registered in the runner that last attempted it (Could not run '<step>' (capability): … — from a program that has it: realm run advance <id> on inspect). With neither, the step is unknown, and the act still names it.
The three paragraphs above were added after version 0.46.0.
pay: description: Pay the refund. execution: auto depends_on: [only_low_risk] handler: payIf the handler throws, the step fails. In the refund example, a payment that was declined was recorded like this:
Step 'pay' failed. Recovery steps are available in next_actions.Handler 'pay' threw: payment provider declined 500Guard steps
Section titled “Guard steps”A guard checks one or more conditions against what earlier steps produced. If every condition is true, the run continues. If any is false, the engine stops the whole run, and its phase becomes aborted.
only_low_risk: description: Stop the run unless the refund is low risk. execution: guard depends_on: [assess] abort_unless: - "assess.risk == 'low'" abort_message: 'High-risk refunds are not automated.'A guard is never handed to the agent. The engine evaluates it in the same call that made it reachable. With a high-risk answer, the reply to the agent’s own step was:
Guard step 'only_low_risk' aborted the run.The record keeps each condition with the value it was checked against:
Output: {"conditions":[{"condition":"assess.risk == 'low'","resolved_value":"low","passed":true}],"aborted":false}Use a guard, not a prompt instruction, for a rule the agent must not be able to talk its way past.
Finalizer steps
Section titled “Finalizer steps”A finalizer runs after the run has ended. It is the place for cleanup and for notifications about how the run went. A finalizer needs a handler and an on_outcome that says which endings it runs for.
close_books: description: Record that the refund run ended, however it ended. execution: finalizer handler: note on_outcome: alwaysWith on_outcome: always, this step ran when the refund completed and also when the guard aborted it. In the aborted run it is the only step that ran after the guard:
Phase: abortedSealed by: guard_abort (only_low_risk)Completed: assess, close_booksA finalizer without a handler is refused when the workflow is loaded:
Invalid workflow: Step 'tidy': execution: finalizer requires 'handler' (handler-only in v1) (step at line 8)A gate is not a kind
Section titled “A gate is not a kind”A human gate is a property of a step, set with trust:. An auto or agent step can carry one. See Human gates and trust levels.
How to choose
Section titled “How to choose”- The work needs a model or a person to think:
agent. - Code can do it, and a wrong result would simply be a bug to fix:
auto. - It is a yes-or-no rule about earlier results, and a “no” must stop everything:
guard. - It must happen at the end, whatever happened before:
finalizer.