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: arrival
Mermaid source for the ordinary request, call, return, resume, and response path.
Note

The 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.

FieldAuthored shapeMeaning
idhandler:…Stable component-scoped identity derived from the node and arrival port. Let Mockflow derive it.
arrivalPortIdexisting input port IDThe input that starts this ordered handler. It cannot be an output port.
originauthored | catalog_default | migrated_reviewProvenance of the handler binding. Specialized component executors may use their built-in behavior when the handler is untouched.
actionsordered HandlerAction[]The action list. The domain cap is 32 actions per handler.
catchesoptional HandlerCatch[]First-match recovery clauses for failures that reach this handler’s waiting call frame.
Tip

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.

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.

Tip

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

ActionFieldAllowed valueRule
all actionsidnon-empty stringUnique within the handler; for_each IDs must also be unique graph-wide for trace correlation.
all actionswhenHandlerConditionOptional expression.v1 guard. A false guard records a route decision, skips this action, and continues.
calledgeId, awaitPortIdoutgoing edge + input portThe return interaction must target awaitPortId. All intrinsic return paths must share the call interaction.
awaitinteractionId, resumePortIdowned interaction + input portThe interaction must have a return edge owned by this component. No request is dispatched by await.
mapmapping, optional transformationidentity or typedTyped mappings require version map-transformation.v1 and refs matching a name ending in .vN. Identity mappings cannot carry a transformation.
for_eachcollection, itemVariable, edgeIdHandlerValueReference + identifier + fan-out edgeThe collection must resolve to an array. The editor restricts itemVariable to [A-Za-z_][A-Za-z0-9_]*.
for_eachmaxItems, mode, maxConcurrencynumber; serial | parallel; optional numbercollection.length must not exceed maxItems. Launch gating uses maxConcurrency ?? 1; mode is retained in the iteration event, so use 1 for serial behavior.
emitedgeIdoutgoing async/event/control/write edgeThe route is dispatched without a call-frame wait. Downstream queue/topic behavior can still create child tokens.
respondedgeId xor respondToresponse edge or arrivalrespondTo arrival requires exactly one compatible reverse edge for every possible incoming interaction.
failcode, optional edgeIdfailure code + optional error edgeThe code must match ^[a-z][a-z0-9_.-]{0,99}$. An edgeId, when present, must reference an outgoing error edge.
assignassignmentsordered { 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.

SourceWhere it is validResolved root
payloadconditions and value referencesCurrent handler payload at the point the action is prepared.
inputconditions and value referencesImmutable handler-entry payload; if no separate input is present, resolution falls back to payload.
variablesconditions and value referencesThe journey’s authored scenario.variables object.
fixtureconditions and value referencesLive fixture state for fixtureNodeId. The component ID must exist in the graph.
arrivalconditions onlyThe 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 }
  ]
}
Tip

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.

OperatorNeeds valueTrue when
equalsyesThe pointer exists and its canonical JSON value equals value.
not_equalsyesThe pointer exists and its canonical JSON value differs from value.
existsnoThe pointer resolves, including when the resolved value is null.
not_existsnoThe 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" }
  ]
}
Note

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 choiceRequired topologyWhat completes it
callRequest/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.
awaitOwned 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_eachFan-out-compatible edge and an array within maxItems.The parent token completes only after all child tokens have completed; later handler actions are invalid.
Tip

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 fieldAuthored shapeSemantics
idstable stringUnique within the handler. Catch clauses are checked in list order.
whenoptional expression.v1 conditionEvaluated against the failure payload, original handler input, variables, live fixtures, and arrival { outcome: failure, code }.
actionsassign | map | call | await | emitRecovery actions run in order before completion. Catch actions cannot be respond, fail, or for_each.
completionoutcome 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 behaviorUseful event kindsReading
whenROUTE_EVALUATED, ROUTE_SELECTEDThe condition was evaluated; selected means the action or Topic subscription was allowed to run.
call, awaitCALL_STARTED, CALLER_WAITING, RESPONSE_RECEIVED, WAIT_COMPLETED, CALL_COMPLETEDShows the request frame, suspension, matching return, and resume.
map, assignTRANSFORM_APPLIEDMap records its mapping metadata; assign records the assignment count and changes the payload snapshot used by later actions.
for_eachCOLLECTION_ITERATION_STARTED, COLLECTION_ITEM_DISPATCHED, TOKEN_FORKEDShows the array count, bound, mode, concurrency, and each emitted child item.
fail, catchesERROR_RAISED, CALL_FRAME_UNWOUND, ERROR_HANDLED, RETRY_SCHEDULED, RETRY_EXHAUSTEDSeparates 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.

BoundaryValueApplies to
handlers per component64Domain validation; one of those must bind to each input port.
actions per handler32Domain validation and the main web editor Add button.
condition clauses8Web editor control; conditions remain versioned graph data.
catches / recovery actions8 / 16Web editor controls. Catch semantics and action validation still apply to imported authored data.
assignments per action16Web editor control; assignments themselves are ordered.
items per for_eachmaxItemsAuthored per action. A larger collection raises collection_limit_exceeded.
scenario maxEvents10,000Hard simulation cap; the default scenario value is 5,000.
root actor tokens1,000Hard start-count cap. A graph with for_each plans up to 10,000 runtime tokens.
payload / aggregate / fixture state256 KiB / 10 MiB / 10 MiBRun guard limits that assignments, iterations, and fixtures can reach.
Note

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 familyWhat it usually meansFirst fix
graph.handler.binding_missing, arrival_port_missing, arrival_port_duplicateEvery 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_requiredHandler 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_invalidThe 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_missingA 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_unreachableActions 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_overlapsAn 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_mismatchPointers 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_ambiguousfor_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_incompatibleTyped 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_invalidLiteral 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_invalidFailure 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_invalidA 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 symptomSource-backed code or eventCheck
Iteration stops immediatelycollection_not_array or collection_limit_exceededResolve the value reference against the current payload/input/variables/fixture and compare its array length with maxItems.
Payload assignment raises an errorpayload_assignment_path_invalid, payload_assignment_path_missing, payload_assignment_target_invalidCheck RFC 6901 spelling, existing parents/indexes, and the current payload shape at that action.
Run drains without a responseUNRESOLVED_CONTINUATIONInspect the call/await interaction ID, every return edge, the declared resume port, and whether the responder actually completes.
Catch recovery runs but cannot finishcatch_completion_edge_missingRejected 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 timesimulation.compile.objectstore_get_actions_unsupportedRemove authored actions from getIn; intrinsic hit/miss routing must remain the source of the return.
Run stops before completionRUN_GUARD_TRIGGERED plus an ENGINE_* codeCheck maxEvents, maxVirtualTimeMs, token/payload/state limits, executor work, or unresolved capacity waits.