Skip to content

Write a step handler

A handler is a function from your project that an auto step runs. Use one when a step needs logic that no built-in adapter covers: a calculation, a check, a call to your own code. At the end of this guide you have a handler that reads its input and its settings, and you know the three ways it can end: return a result, fail, or stop the run.

You need a workflow you can run. Write your first workflow makes one.

Create registry.mjs beside workflow.yaml. The file’s default export has a handlers map. Each handler has an id and an execute function:

export default {
handlers: {
order_total: {
id: 'order_total',
async execute(inputs, context) {
const quantity = inputs.params.quantity;
const unitPrice = context.config.unit_price;
if (typeof quantity !== 'number' || quantity <= 0) {
throw new Error(`quantity must be a positive number, got ${JSON.stringify(quantity)}`);
}
if (quantity > 1000) {
return { abort: { message: 'Orders over 1000 units are quoted by hand.' } };
}
return { data: { total: quantity * unitPrice } };
},
},
},
};

execute receives two things:

Argument Holds
inputs.params The step’s input, built by the step’s input_map.
context.config The step’s config block from the workflow file.
context.run_params The parameters the run was started with.
context.resources The outputs of earlier steps, by step name.
context.run_id The ID of the run.

Name the file with extensions at the top of the workflow, and name the handler on the step:

id: price
name: Price an order
version: 1
extensions: ./registry.mjs
params_schema:
type: object
additionalProperties: false
required: [quantity]
properties:
quantity:
type: number
steps:
total:
description: Work out the order total.
execution: auto
handler: order_total
config:
unit_price: 12.5
input_map:
quantity: run.params.quantity
confirm:
description: Say whether the total looks right.
execution: agent
depends_on: [total]
prompt: 'The total is {{ context.resources.total.total }}. Does that look right?'
input_schema:
type: object
additionalProperties: false
required: [ok]
properties:
ok:
type: boolean

The handler value must match a key in the handlers map.

Check the file:

Terminal window
realm workflow validate ./

It prints:

Valid: price v1 (2 steps)
Extensions: ./registry.mjs (adapters: 0, handlers: 1, processors: 0)
ℹ 1 step ready for structured_output: strict — run 'realm workflow validate --explain' for detail (REALM_NO_NUDGE=1 to silence).

Notice the Extensions line. It confirms that Realm loaded your file and found one handler. Then register:

Terminal window
realm workflow register ./

Start a run with a quantity of 4 from an assistant connected over MCP. The engine runs total in the first call, start_run, whose reply says so, and the next step’s prompt shows the result:

Step 'total' completed. Ready for the agent: 'confirm'.
The total is 50. Does that look right?

Version 0.46.0 and earlier say 1 step(s) now available. instead of naming the step.

The record keeps the handler’s input and output:

1. total success 1ms hash: 4e944dad
Taken by: ops@server-1 (from the OS user, via mcp-stdio)
Input: {"quantity":4}
Resolved: {"quantity":4}
Output: {"total":50}

Return { data: … } when the work is done. The object becomes the step’s output. Later steps read it as context.resources.<step>. Return data for every outcome the workflow should deal with, including “nothing found”.

Throw an error when the step cannot do its work. The step fails. With a quantity of -1:

Step 'total' failed. Run is terminated.
ENGINE_HANDLER_FAILED: Handler 'order_total' threw: quantity must be a positive number, got -1

The run ends as failed, unless the workflow has a step that handles the failure. See Handle failure.

Return { abort: { message } } when the run should stop on purpose. This is for a condition you expected, where carrying on makes no sense. With a quantity of 5000:

Phase: aborted
Sealed by: handler_abort (total)
Cause: Handler 'total' aborted the run: Orders over 1000 units are quoted by hand.

The run ends as aborted, not failed, and every later step is skipped.

  • Step 'total' is blocked: its handler 'order_totl' is not registered in this runner. (on the MCP reply of the call that attempted the step: its context_hint for advance_run and execute_step, its warnings for start_run; realm agent prints ⚠ Step 'total' is blocked: handler 'order_totl' is not registered in this runner. The run is NOT failed — add handler 'order_totl' and re-attach (…)). Version 0.46.0 has no advance_run and returns this block from start_run as a status: error reply, not in warnings. The handler name on the step does not match a key in your handlers map. realm workflow validate does not catch this; it shows up when the step runs. The run is not ended: correct the name, register again, and the step can run.
  • Handler step 'total' aborted the run: undefined Your handler returned abort without a message. Use { abort: { message: '…' } }.