Handlers
A handler is an ordered, bounded program attached to one input port. It reads the current arrival, optionally changes or checks its payload, sends work along authored edges, waits for explicit returns when needed, and completes or raises a modeled failure.
The mental model
Think of a handler as a small state machine, not as a function that runs once and disappears. An authored handler starts when a token arrives at its arrivalPortId. Actions run top to bottom. A call or await stores the next action index; the matching return schedules a resume in the same handler. Arespond sends a payload back through a response path, and a fail enters the failure path.
sequenceDiagram
participant C as Caller
participant S as Service
participant D as Dependency
C->>S: arrival on an input port
S->>D: call (current payload)
Note over S: handler pauses
D-->>S: matching return edge
Note over S: resume next action in the same handler
S-->>C: respondTo: arrivalThe names in the configuration tables below are authored graph fields. Names such as CALL_STARTED, WAIT_COMPLETED, TRANSFORM_APPLIED, and ERROR_HANDLED are run-time event kinds written to the simulation artifact; they are evidence of what happened, not additional actions to author.
Where handlers live
Select a component and open the Behavior tab. TheArrival handlers section shows one handler per input port. A graph must keep exactly one handler for every input port; a newly created component receives empty catalog_default bindings. Adding or editing actions changes the handler origin to authored. A migrated_review binding is a warning that the persisted behavior needs confirmation.
| Field | Authored shape | Meaning |
|---|---|---|
id | handler:… | Stable component-scoped identity derived from the node and arrival port. Let Mockflow derive it. |
arrivalPortId | existing input port ID | The input that starts this ordered handler. It cannot be an output port. |
origin | authored | catalog_default | migrated_review | Provenance of the handler binding. Specialized component executors may use their built-in behavior when the handler is untouched. |
actions | ordered HandlerAction[] | The action list. The domain cap is 32 actions per handler. |
catches | optional HandlerCatch[] | First-match recovery clauses for failures that reach this handler’s waiting call frame. |
An empty catalog-default handler is not necessarily a no-op: the component executor can provide intrinsic cache, database, queue, topic, workflow, or boundary behavior. Authored actions take over on the component paths that support them. Object Store getIn is a deliberate exception: authored get actions are rejected at compile time because hit/miss routing is intrinsic.
The eight handler actions
These are the current HandlerActionKind values. The web editor’s action picker exposes every kind except for_each; imported or agent-authored for_each actions still render their collection and item fields and are validated and simulated by the same runtime.
Use call when this handler needs a synchronous request or data-access operation and should continue after the matching return arrives.
Example: check stock before confirming an order
{
"id": "ask-inventory",
"kind": "call",
"edgeId": "inventory-request",
"awaitPortId": "stockResponseIn"
}In simple English: The service sends the current order to Inventory, pauses here, and runs its next action only when the return reaches stockResponseIn.
Use await when another modeled interaction already created the request. It listens for the matching return; it does not send a new request.
Example: wait for a payment result
{
"id": "wait-for-payment",
"kind": "await",
"interactionId": "payment-request",
"resumePortId": "paymentResponseIn"
}In simple English: Something else already started payment-request. When its response arrives at paymentResponseIn, this handler resumes.
Use map to record an identity mapping or a versioned typed mapping. The generic handler map records the mapping; it does not rewrite JSON values.
Example: say that the payload keeps its shape
{
"id": "keep-order-shape",
"kind": "map",
"mapping": "identity"
}In simple English: The payload stays the same, but the run records that the next step accepts the same shape. Typed mappings use a map-transformation.v1 declaration.
Use for_each when an array should fan out into separate child tokens. It is bounded, and it must be the last action in the handler.
Example: send each order item to a worker
{
"id": "send-items",
"kind": "for_each",
"collection": {
"expressionVersion": "expression.v1",
"source": "payload",
"pointer": "/items"
},
"itemVariable": "item",
"edgeId": "item-worker",
"maxItems": 50,
"mode": "serial"
}In simple English: Mockflow reads payload.items, sends at most 50 child items through item-worker, and sends them one at a time. The parent finishes after the children finish.
Use emit for an asynchronous, event, control, or write edge when the handler should continue immediately instead of opening a call-and-return wait.
Example: notify shipping that an order was paid
{
"id": "notify-shipping",
"kind": "emit",
"edgeId": "shipping-event"
}In simple English: The current payload is sent to shipping-event, and the next action runs without waiting for a reply.
Use respond to send the current payload on an explicit response edge or back through the response path of the original arrival.
Example: return the result to whoever called
{
"id": "reply-to-checkout",
"kind": "respond",
"respondTo": "arrival"
}In simple English: Mockflow finds the compatible response route belonging to the original arrival and sends the current result back to that caller.
Use fail when the handler cannot continue. A failure can go down an error edge, or enter retry, catch, and unwind handling.
Example: reject an invalid address
{
"id": "reject-order",
"kind": "fail",
"code": "invalid_address",
"edgeId": "error-out"
}In simple English: The action stops the normal path and records invalid_address. Because error-out exists, the failure is routed there.
Use assign to apply ordered literal JSON values at JSON Pointer paths. Assignments are made to an immutable copy and require graph.v7 behavior.
Example: mark an order as ready
{
"id": "mark-ready",
"kind": "assign",
"assignments": [
{ "pointer": "/status", "value": "ready" },
{ "pointer": "/attempts", "value": 1 }
]
}In simple English: The handler keeps the original payload unchanged, creates the next payload with status and attempts updated, and gives that new value to later actions.
Edge-backed actions select stable graph edges, not labels typed into the action. The allowed edge kinds are intentionally narrow: calls use synchronous request or data-access request/write edges; emits and iterations use asynchronous message, event publish, control, or write data-access edges; responses use synchronous response, response-role data-access, ACK, or NACK edges; failures use error edges.
respond does not make later actions unreachable by itself. The executor continues through the ordered list. The validator rejects overlapping response paths, so author one final response per path or give response branches mutually exclusive when guards.
Action fields in detail
| Action | Field | Allowed value | Rule |
|---|---|---|---|
| all actions | id | non-empty string | Unique within the handler; for_each IDs must also be unique graph-wide for trace correlation. |
| all actions | when | HandlerCondition | Optional expression.v1 guard. A false guard records a route decision, skips this action, and continues. |
call | edgeId, awaitPortId | outgoing edge + input port | The return interaction must target awaitPortId. All intrinsic return paths must share the call interaction. |
await | interactionId, resumePortId | owned interaction + input port | The interaction must have a return edge owned by this component. No request is dispatched by await. |
map | mapping, optional transformation | identity or typed | Typed mappings require version map-transformation.v1 and refs matching a name ending in .vN. Identity mappings cannot carry a transformation. |
for_each | collection, itemVariable, edgeId | HandlerValueReference + identifier + fan-out edge | The collection must resolve to an array. The editor restricts itemVariable to [A-Za-z_][A-Za-z0-9_]*. |
for_each | maxItems, mode, maxConcurrency | number; serial | parallel; optional number | collection.length must not exceed maxItems. Launch gating uses maxConcurrency ?? 1; mode is retained in the iteration event, so use 1 for serial behavior. |
emit | edgeId | outgoing async/event/control/write edge | The route is dispatched without a call-frame wait. Downstream queue/topic behavior can still create child tokens. |
respond | edgeId xor respondTo | response edge or arrival | respondTo arrival requires exactly one compatible reverse edge for every possible incoming interaction. |
fail | code, optional edgeId | failure code + optional error edge | The code must match ^[a-z][a-z0-9_.-]{0,99}$. An edgeId, when present, must reference an outgoing error edge. |
assign | assignments | ordered { pointer, value }[] | Requires graph.v7. Each value is a JSON literal, not a value reference or expression. |
Payloads, pointers, and value references
A handler has two useful payload views. payload is the current value and can change after an assign or a returned call payload. input is the immutable payload captured when the handler first received the arrival; it remains available through calls, awaits, retries, and catches. This is why a post-call guard can compare the returned payload with the original request.
| Source | Where it is valid | Resolved root |
|---|---|---|
payload | conditions and value references | Current handler payload at the point the action is prepared. |
input | conditions and value references | Immutable handler-entry payload; if no separate input is present, resolution falls back to payload. |
variables | conditions and value references | The journey’s authored scenario.variables object. |
fixture | conditions and value references | Live fixture state for fixtureNodeId. The component ID must exist in the graph. |
arrival | conditions only | The current route outcome: { outcome, edgeId, code? }. Useful pointers are /outcome, /edgeId, and /code. |
Pointers use RFC 6901 syntax. An empty pointer reads or replaces the whole value. A path such as /order/items/0 traverses existing object members and numeric array indexes; ~1 represents / inside a key and ~0 represents ~.
Literal assignment fragment
{
"id": "mark-ready",
"kind": "assign",
"assignments": [
{ "pointer": "/status", "value": "ready" },
{ "pointer": "/items/0/approved", "value": true }
]
}Assignments run in list order on immutable copies. Parent objects and array indexes must already exist; array append (-) is invalid. An empty pointer replaces the whole payload. Unsafe segments __proto__, constructor, and prototype are rejected. Run-time failures are named payload_assignment_path_invalid, payload_assignment_path_missing, or payload_assignment_target_invalid and produce ERROR_RAISED.
A for_each value reference must resolve to an array. Each child payload starts from the parent object with a payload-selected collection removed when the reference is a payload path, then adds/itemVariable and /iterationIndex. An empty collection completes the parent without dispatching children.
A worked request handler
This fragment keeps the useful checkout mental model from the original guide, while showing the exact authored fields. The edge IDs must already exist on the graph, and the response edge must be paired by its interaction identity.
Handler: On request in — Checkout service
{
"arrivalPortId": "requestIn",
"origin": "authored",
"actions": [
{ "id": "mark-checking", "kind": "assign", "assignments": [{ "pointer": "/status", "value": "checking" }] },
{ "id": "authorize", "kind": "call", "edgeId": "auth-request", "awaitPortId": "responseIn" },
{ "id": "fulfill", "kind": "emit", "edgeId": "fulfillment-queue" },
{ "id": "reply", "kind": "respond", "respondTo": "arrival" }
]
}Order matters: the assignment changes the payload used by the call, the call waits for its matching return, the emit does not wait, and the arrival response uses the original caller route. If the call fails, a matching catch can recover before the response completes.
Handlers versus component settings
The Behavior tab also contains component settings such as cache TTL, service duration, queue attempts, or an external dependency outcome. Settings describe intrinsic component behavior; handlers describe what the component does when a token arrives. Connections provide the routes that edge-backed actions are allowed to use. A simulation is the combination of all three.
Conditions and edge matching
Add Run only when… to an action to attach an expression.v1 HandlerCondition. Choose all or any; a false action guard skips only that action and the list continues. Equality is structural JSON equality, so object key order does not change a match. A missing path makes equals and not_equals false; use exists or not_exists when absence is the condition you mean.
| Operator | Needs value | True when |
|---|---|---|
equals | yes | The pointer exists and its canonical JSON value equals value. |
not_equals | yes | The pointer exists and its canonical JSON value differs from value. |
exists | no | The pointer resolves, including when the resolved value is null. |
not_exists | no | The pointer cannot be resolved. |
Action condition fragment
"when": {
"expressionVersion": "expression.v1",
"match": "all",
"clauses": [
{ "source": "payload", "pointer": "/status", "operator": "equals", "value": "ready" },
{ "source": "fixture", "fixtureNodeId": "session-cache", "pointer": "/enabled", "operator": "exists" }
]
}Edge matching is a separate feature. GraphEdge.when uses expression.v2 and is valid only on a Topic’s deliverOut subscription edge. It adds contains, which is true only when the selected value is an array containing a canonically equal member. When a topic publishes, each subscription edge is evaluated independently; selected edges are delivered and unselected edges are skipped.
Topic subscription edge condition
"when": {
"expressionVersion": "expression.v2",
"match": "all",
"clauses": [
{ "source": "payload", "pointer": "/channels", "operator": "contains", "value": "email" }
]
}Edge conditions are evaluated against the publish invocation’s payload, immutable input, journey variables, and live fixture state. They do not receive a handler arrival outcome. Keep source: "arrival" for action/catch conditions.
Arrival, return, and completion semantics
An input arrival starts one handler. A call sends a request and saves the next action index. A return edge is matched by interaction identity, reverse endpoints, edge kind, and return-port role; the handler on that return port is not invoked for the matched return. Instead, the original handler resumes at the next action with the returned payload and the original input still available.
| Authored choice | Required topology | What completes it |
|---|---|---|
call | Request/data-access edge plus one or more matching reverse return edges to awaitPortId. | The saved handler resumes when the return arrives; post-call work belongs after call in the same handler. |
await | Owned interactionId and a return edge to resumePortId. | A separate request or initial event must produce the matching return; otherwise the run ends with UNRESOLVED_CONTINUATION. |
respondTo: "arrival" | For each incoming edge, exactly one reverse compatible response edge with the same interaction ID. | The response goes back to the caller represented by the original arrival edge. |
for_each | Fan-out-compatible edge and an array within maxItems. | The parent token completes only after all child tokens have completed; later handler actions are invalid. |
A call already owns its suspension. A directly following await with the same interaction and resume port is rejected as graph.handler.redundant_await_after_call. This is the most common reason an apparently correct handler never reaches its response.
Failure handling and catches
A fail action stops the current action list. If its optional edgeId points to an error edge, the failure is routed explicitly. Otherwise the simulator creates a failed invocation outcome. Interaction resilience can retry timeout, network_error, http_429, or http_5xx outcomes on a synchronous interaction, subject to that interaction’s maxAttempts and backoff policy. After retries are exhausted, the runtime searches catches from first to last.
| Catch field | Authored shape | Semantics |
|---|---|---|
id | stable string | Unique within the handler. Catch clauses are checked in list order. |
when | optional expression.v1 condition | Evaluated against the failure payload, original handler input, variables, live fixtures, and arrival { outcome: failure, code }. |
actions | assign | map | call | await | emit | Recovery actions run in order before completion. Catch actions cannot be respond, fail, or for_each. |
completion | outcome plus edgeId or respondTo: "arrival" | rejected uses an error edge; degraded uses a response/ACK/NACK-compatible edge. Arrival completion resolves the matching original caller route. |
A matching catch unwinds the failed call frames, runs its recovery actions, and records ERROR_HANDLED. An unmatched failure propagates through waiting call frames; if no caller catch handles it, the token fails. A catch-all has no when and must be the final catch clause.
Failure-code catch fragment
{
"id": "recover-timeout",
"when": {
"expressionVersion": "expression.v1",
"match": "all",
"clauses": [
{ "source": "arrival", "pointer": "/code", "operator": "equals", "value": "dependency_timeout" }
]
},
"actions": [
{ "id": "mark-degraded", "kind": "assign", "assignments": [{ "pointer": "/status", "value": "degraded" }] }
],
"completion": { "edgeId": "fallback-response", "outcome": "degraded" }
}What to look for in a run
The run artifact records decisions and effects in deterministic order. Use the action or edge ID in event details to connect an authored field to the run-time evidence.
| Authored behavior | Useful event kinds | Reading |
|---|---|---|
when | ROUTE_EVALUATED, ROUTE_SELECTED | The condition was evaluated; selected means the action or Topic subscription was allowed to run. |
call, await | CALL_STARTED, CALLER_WAITING, RESPONSE_RECEIVED, WAIT_COMPLETED, CALL_COMPLETED | Shows the request frame, suspension, matching return, and resume. |
map, assign | TRANSFORM_APPLIED | Map records its mapping metadata; assign records the assignment count and changes the payload snapshot used by later actions. |
for_each | COLLECTION_ITERATION_STARTED, COLLECTION_ITEM_DISPATCHED, TOKEN_FORKED | Shows the array count, bound, mode, concurrency, and each emitted child item. |
fail, catches | ERROR_RAISED, CALL_FRAME_UNWOUND, ERROR_HANDLED, RETRY_SCHEDULED, RETRY_EXHAUSTED | Separates the original failure, retry lifecycle, propagation, and handled outcome. |
Ordering and limits
Action order is the authored array order. The compiler keeps edge dispatch deterministic by ascending priority, then edge ID. A condition can skip an action, but it cannot reorder later actions. Calls and awaits create suspension points; for_each is the only action that must be terminal.
| Boundary | Value | Applies to |
|---|---|---|
| handlers per component | 64 | Domain validation; one of those must bind to each input port. |
| actions per handler | 32 | Domain validation and the main web editor Add button. |
| condition clauses | 8 | Web editor control; conditions remain versioned graph data. |
| catches / recovery actions | 8 / 16 | Web editor controls. Catch semantics and action validation still apply to imported authored data. |
| assignments per action | 16 | Web editor control; assignments themselves are ordered. |
| items per for_each | maxItems | Authored per action. A larger collection raises collection_limit_exceeded. |
| scenario maxEvents | 10,000 | Hard simulation cap; the default scenario value is 5,000. |
| root actor tokens | 1,000 | Hard start-count cap. A graph with for_each plans up to 10,000 runtime tokens. |
| payload / aggregate / fixture state | 256 KiB / 10 MiB / 10 MiB | Run guard limits that assignments, iterations, and fixtures can reach. |
The web editor currently hides for_each from the main action picker and does not expose its bound/mode controls; its default constructor uses maxItems: 100 and mode: "serial". The persisted action fields remain authoritative, and the simulator uses their stored values.
Troubleshooting validation and runs
The editor shows handler diagnostics with a source path and remediation. Fix the graph diagnostic before interpreting a run failure; compile-time errors mean the action was never executable.
| Diagnostic family | What it usually means | First fix |
|---|---|---|
graph.handler.binding_missing, arrival_port_missing, arrival_port_duplicate | Every input port needs exactly one handler bound to an existing input port. | Add or merge the handler; do not create a second handler for the same arrival port. |
graph.handler.duplicate_id, canonical_identity_mismatch, review_required | Handler identity is component-scoped and migrated bindings are explicitly marked for review. | Keep the server-derived handler ID and confirm migrated bindings before running. |
edge_missing, edge_kind_invalid | The referenced edge must leave this node and have the role/kind required by the action. | Select an outgoing edge from the editor’s action field; an ACK/NACK data edge is a response, not an emit. |
return_path_missing, return_path_incomplete, resume_port_missing | A call/await needs a reverse return with the same interaction identity and the declared resume port. Cache and Object Store calls need every intrinsic return branch. | Pair the request and all response, hit/miss, object/missing, ACK/NACK paths before adding the action. |
await_port_actions_unreachable | Actions on the handler attached to a call’s return port do not run for that matched return. | Move post-call actions directly after the call in the original handler and leave the return-port handler empty. |
arrival_response_source_missing, arrival_response_ambiguous, response_path_overlaps | An arrival response needs an incoming caller edge and exactly one compatible return per possible arrival. Two response actions must not overlap on one path. | Use one explicit response edge or mutually exclusive guards; for arrival responses, keep interaction IDs and reverse endpoints aligned. |
condition_pointer_invalid, condition_fixture_missing, simulation.compile.condition_expression_version_mismatch | Pointers must be bounded RFC 6901 pointers, fixtures must name existing components, and handler conditions must use the journey expression version. | Use /field or an empty pointer, select an existing fixture component, and keep action conditions at expression.v1. |
iteration_edge_invalid, iteration_must_be_terminal, iteration_id_ambiguous | for_each needs a fan-out-compatible edge, must be the last handler action, and has a graph-wide unique action ID. | Route the bounded children through an asynchronous/event/control/write edge and move all earlier work before for_each. |
map_transformation_required, map_type_ref_invalid, map_type_incompatible | Typed maps need versioned refs ending in .vN, and adjacent typed maps must connect outputTypeRef to inputTypeRef. | Use a declaration such as orders.v1 → fulfillment.v1 and keep the chain compatible. |
assignment_version_required, assignment_pointer_invalid | Literal assignments are graph.v7 behavior and reject malformed or unsafe JSON Pointer paths. | Migrate to graph.v7 and use existing object parents/array indexes; never use __proto__, constructor, or prototype segments. |
failure_code_invalid | Failure codes are lowercase machine-readable names matching ^[a-z][a-z0-9_.-]{0,99}$. | Use a code such as dependency_timeout or invalid_token. |
catch_all_order_invalid, catch_action_kind_invalid, catch_completion_edge_missing, catch_rejection_edge_invalid, catch_degraded_edge_invalid | A catch-all is last; catch recovery cannot use respond, fail, or for_each; rejected completion uses an error edge and degraded completion uses a response-compatible edge. | Order specific catches first, keep recovery actions before the completion route, and choose the edge for the selected outcome. |
| Run-time symptom | Source-backed code or event | Check |
|---|---|---|
| Iteration stops immediately | collection_not_array or collection_limit_exceeded | Resolve the value reference against the current payload/input/variables/fixture and compare its array length with maxItems. |
| Payload assignment raises an error | payload_assignment_path_invalid, payload_assignment_path_missing, payload_assignment_target_invalid | Check RFC 6901 spelling, existing parents/indexes, and the current payload shape at that action. |
| Run drains without a response | UNRESOLVED_CONTINUATION | Inspect the call/await interaction ID, every return edge, the declared resume port, and whether the responder actually completes. |
| Catch recovery runs but cannot finish | catch_completion_edge_missing | Rejected catches need an error edge; degraded catches need a response/ACK/NACK-compatible edge or a valid arrival completion. |
| Object Store get handler is refused at compile time | simulation.compile.objectstore_get_actions_unsupported | Remove authored actions from getIn; intrinsic hit/miss routing must remain the source of the return. |
| Run stops before completion | RUN_GUARD_TRIGGERED plus an ENGINE_* code | Check maxEvents, maxVirtualTimeMs, token/payload/state limits, executor work, or unresolved capacity waits. |