WorkflowDefinition — the JSON contract you build in the workflow builder or generate with the AI Composer — into an executable graph and runs it. This page documents the engine’s contract: how a workflow compiles, the 9 node types and what each writes to state, how edges and the virtual __start__/__end__ endpoints work, how run state and reducers behave, and how the {{nodeId.path}} reference syntax resolves data between steps.
This is the conceptual model behind every run. For the per-node configuration reference, see the individual node type pages. For the wire-level run lifecycle and events, see SSE run streaming.
The mental model
A workflow is a directed graph of nodes connected by edges, executed over a shared run state. The engine evaluates the graph one node at a time (or in parallel where a loop fans out), and every node writes its result back into state. Later nodes read earlier results through the{{nodeId.path}} reference syntax.
Four pieces make up the contract:
Nodes
The 9 step types —
llm, tool, agent, function, conditional, interrupt, transformer, guardrails, knowledge. Each node writes its result to run state under its own id.Edges
Directed connections between nodes. The virtual
__start__ and __end__ endpoints mark where execution enters and leaves the graph.State
A dynamic dictionary that flows through the run. Its fields come from your
state_schema, plus one auto-added field per node id, plus any loop fields.References
The
{{nodeId.path}} syntax that pulls a value out of state — typed when the whole field is a single reference, templated when embedded in a string.WorkflowDefinition into an executable state graph, runs it in a background task, persists run state through the managed checkpointer, and streams node and run events over SSE. You do not manage the engine directly — you describe the graph in JSON and the engine handles compilation, persistence, and streaming.
The WorkflowDefinition
A workflow is a single JSON object. These are its top-level fields.
object
Workflow name, description, version, and tags. The engine denormalizes some of these for fast filtering, but the workflow definition is the source of truth.
object
Execution configuration. Several fields are accepted on the wire but forced to constant values by the engine regardless of what you send (see Execution config below).
object
Declares the named fields that flow through the run, their types, and their reducers. See Run state.
array
required
The list of
NodeDefinition objects. The virtual __start__ and __end__ endpoints must not appear here — they are wired automatically and a validator rejects a definition that lists them as nodes.array
required
The list of
EdgeDefinition objects connecting nodes. Edges may also express conditional branches and loops.string
default:"__start__"
The node where execution begins. Defaults to the virtual
__start__ endpoint.object
The canvas
{x, y} of the __start__ endpoint. Visual only — it has no effect on execution.The definition is parsed with unknown top-level keys ignored rather than causing an error, so build against the fields documented here.
Execution config
config accepts the fields below, but the engine overrides most of them with fixed values. You can send them, but only recursion_limit is honored from your input.
A run can override
recursion_limit at execution time through its ExecutionConfig (thread_id?, recursion_limit?, checkpoint_id?); otherwise the workflow’s configured limit applies.
Nodes
A workflow has exactly 9 node types. Every node, whatever its type, follows the same output convention: it writes its result to run state under its ownid. A node with id: "node_llm_1" produces state["node_llm_1"], and later nodes reference it as {{node_llm_1}}.
Several configs expose a legacy
output_key field. It is deprecated — node output always lands under the node id, not a custom key. Do not rely on output_key, input_keys, or prompt_template; they are kept only for backward compatibility.NodeDefinition. The full per-node parameter reference lives on the dedicated node pages; the table below is the index.
An unknown
type fails compilation with an “Unsupported node type” error. See the node types overview for the complete list and how each writes to state.
The NodeDefinition
Every node, regardless of type, shares this shape.
string
required
The node’s unique identifier within the workflow. This is the key its output is written under in run state, and the name later nodes reference with
{{id}}.string
required
One of the 9 node types.
string
A human-readable label shown in the builder.
string
An optional description of what the node does.
boolean
default:"true"
Whether the node participates in the run. A disabled node is dropped at compile time and its edges are rewired to the next enabled target, so you can disable a step without rewiring the graph by hand. A
null value is treated as true.number
required
The node’s horizontal canvas position. Visual only.
number
required
The node’s vertical canvas position. Visual only.
object
Optional per-node retry policy. Most node types are retry-wrapped; the
interrupt node is not retry-wrapped, because retrying a human pause is meaningless.object
required
Exactly one type-specific configuration slot, matching the node
type (for example, llm_config for an llm node). The builder may send a wrapped {"config": {...}} form; the engine normalizes it to the type-specific key automatically.What each node type writes to state
Because every node writes under its ownid, knowing each type’s output shape tells you exactly what later nodes can reference.
llm — language-model call
llm — language-model call
Resolves the system and user prompt templates, calls the model, and returns the response text under the node id. If you configure a
structured_output_schema, the engine asks the model for structured JSON and returns the parsed object instead, falling back to JSON extraction from the raw response if structured output fails. LLM usage is metered in credits after each call. See the LLM node.tool — single integration action
tool — single integration action
Resolves the
input_mapping (which uses {{...}} references), loads the tool for the configured integration and credential, invokes it, and returns the unwrapped result under the node id. A {success, action, result: {...}} wrapper is flattened to the root, with result kept for backward-compatible .result.field paths. The tool node honors both parameter_defaults and parameter_overrides. See the Tool node.agent — autonomous tool loop
agent — autonomous tool loop
Binds the configured tools to the model and runs a tool-calling loop up to
max_iterations (default 10): each turn invokes the model, executes any tool calls, and appends the results. The agent applies parameter_defaults only — parameter_overrides are ignored for agents. It raises if the resolved input is empty, and meters LLM usage every iteration. See the Agent node.function — built-in registry function
function — built-in registry function
Looks up a built-in function by
function_name and executes it. On success it writes result.data under the node id; on failure it writes {error, success: false, ...} so downstream nodes can branch on the error. There are exactly four built-ins: http_request, send_webhook, validate_schema, and validate_workflow_schema. See the Function node.conditional — branch or loop
conditional — branch or loop
Has a
condition_type of "expression", "llm", or "loop". An expression node evaluates ordered branches and stores the matched target; the engine then adds conditional edges that route on it. An LLM node asks the model to pick a route. A loop node acts as the loop controller. See the Conditional node.interrupt — human-in-the-loop pause
interrupt — human-in-the-loop pause
Builds a question payload (
message, data, optional resume_schema and examples) and pauses the run. The active client receives an interrupt event; the run status becomes interrupted. On resume, the human’s value is stored under the node id. This node is not retry-wrapped. See the Interrupt node.transformer — reshape data
transformer — reshape data
Resolves a
source reference and applies an ordered list of operations (string, object, array, type, date, and math operations). The transformed value is written under the node id. See the Transformer node.guardrails — validate content
guardrails — validate content
Runs JSON-schema, regex, and PII checks against a
source reference, with an on_failure action of block, warn, transform, or route. The hallucination check is a placeholder and returns a “coming soon” marker. The result records which validations ran, whether the content was valid, and any findings. See the Guardrails node.knowledge — retrieve from a knowledge base
knowledge — retrieve from a knowledge base
Resolves a
query reference and retrieves from the configured knowledge base. Managed (modulexdb) retrieval reserves and records credits; bring-your-own-key vector stores are uncosted. The output is formatted as chunks, context, or both. See the Knowledge node.Edges and the virtual endpoints
Edges are directed connections between nodes. Two endpoint names are virtual:__start__ and __end__. They mark where execution enters and leaves the graph but are not real nodes — they have no configuration and must not appear in the nodes array.
Edges with the virtual endpoints
1
__start__ wires to graph entry
An edge from
__start__ connects the engine’s entry point to your first node. The workflow’s entry_point defaults to __start__.2
__end__ wires to graph exit
An edge to
__end__ terminates a branch.3
Terminal nodes are auto-connected
Any enabled node that has no outgoing edge is automatically connected to the exit. An explicit edge to
__end__ is therefore optional — leave it out and the node still terminates correctly.4
Disabled nodes are bypassed
A node with
enabled: false is dropped at compile time and its edges are rewired to the next enabled target, recursively.5
Fan-in is guarded
A node with more than one incoming edge is wrapped so it runs once after its predecessors converge, rather than once per incoming edge.
Run state
State is a single dictionary that flows through the run. Its fields come from three sources, combined at compile time:- Your declared fields — the
state_schema.fieldsyou define. - One field per node id — the engine auto-adds a field (typed
Any) for every node so its output streams correctly. This is why{{node_id}}always resolves to that node’s output. - Loop fields — auto-added bookkeeping fields for any loop (see Loops).
Declaring a state field
Each entry instate_schema.fields is a StateField.
string
required
One of
string, integer, float, boolean, object, array, or messages (mapped to Python str, int, float, bool, dict, list, and list respectively).string
An optional description of the field.
string
default:"none"
How concurrent or repeated writes to this field merge. One of
none, add, or update (see Reducers).boolean
Whether the field must be present.
any
The value applied at run start if no input overrides it.
defaults first, then type-appropriate empty values for anything without a default, then your run input on top — so input always wins.
Reducers
A reducer decides what happens when a field is written more than once (for example, by parallel branches of a loop).reducer
Plain replace — the new value overwrites the old. This is the default.
reducer
A smart-append reducer. For
array fields it appends, auto-wrapping a scalar into a single-element list ([] + "x" becomes ["x"]); for other types it uses addition. Use this to accumulate results across loop iterations.reducer
A type-safe dictionary merge. New keys are added and existing keys are overwritten, while the rest of the object is preserved.
The reducer matters most inside loops and parallel branches, where the same field is written multiple times. For a straight-line workflow, the default
none (replace) is usually what you want. See Variables & references for worked examples.References — {{nodeId.path}}
References are how data moves between nodes. The syntax is {{nodeId.path}}, where nodeId is a node’s id and path is an optional dot-and-bracket path into that node’s output.
Reference forms
- Whole-value reference
- Templated string
- Array spread
When a field is exactly one
{{...}} reference, the engine returns the native typed value — a number stays a number, an object stays an object, an array stays an array. Use this to pass structured data between nodes without stringifying it.Typed pass-through
- Missing paths resolve to nothing. An out-of-range index, a missing key, or a malformed bracket path resolves to
nullsilently rather than raising — design downstream nodes to toleratenull. input_mappingskips empty values. Keys whose resolved value isnullor an empty string are dropped from the mapping, so they never reach the tool or function.- Mixed and nested structures recurse. A reference inside a nested object or list is resolved per-item, preserving the surrounding structure.
Loops
A loop repeats a section of the graph. Loops are expressed either as an edge whose condition type isloop, or as a conditional node whose condition_type is "loop" (the node acts as the loop controller). A loop has three modes:
loop mode
A fixed number of iterations, from
iterations or a iterations_ref reference.loop mode
One iteration per item in a
collection (a {{...}} reference). Set parallel: true to fan the iterations out concurrently; otherwise they run in sequence.loop mode
Repeats while a condition expression is true, bounded by
max_iterations (default 100) as a safety stop.loop_id:
A loop’s
exit_target defaults to __end__ — an empty or missing exit target is normalized to the virtual end. Set body_target to the first node of the loop body, and optionally body_end (it defaults to body_target) to mark where one iteration ends.
recursion_limit (default 500) caps total node transitions across the whole run, including every loop iteration. A long-running foreach over a large collection can exhaust it — raise the limit on config or at run time, or set parallel: true to fan out instead of iterating.Checkpointer and threads
Every run’s state is persisted by the managed checkpointer, backed by the same managed datastore as the rest of ModuleX. Checkpointing is always on — theenable_checkpointing and checkpointer_type config fields are forced, so you cannot turn it off or switch to an in-memory saver.
The checkpointer is what makes human-in-the-loop possible: when an interrupt node pauses a run, its full state is checkpointed, and a resume call restores that state and continues the same run from where it stopped.
A thread is the unit of checkpoint continuity, identified by a thread_id on the run config. When you run a workflow from a chat, the chat’s id is reused as the thread id, so the run shares the conversation’s checkpoint thread; an ephemeral run without a thread gets a generated id.
Run lifecycle and events
A run executes in a background task: it persists a durable run row, streams ametadata event, applies state defaults, then steps the graph node by node, publishing a node_update event as each node completes. An interrupt pauses the run; done, error, or cancelled terminate it. A run has a default timeout of one hour.
You observe a run by listening to its SSE stream. The wire is a sequence of data: <json>\n\n frames with no event: line — the event discriminator is the type key inside each JSON payload, so a client parses each frame and switches on .type.
SSE frames for a successful two-node run
node_update carries node and output keys, not the node_id/status fields of the internal typed model. Build clients against the wire shape shown here. A {"type": "heartbeat"} keepalive is injected every 15 seconds so the connection never idles closed, and on reconnect the stream replays the run’s buffered history before tailing live. The full event taxonomy, interrupt and resume frames, and reconnection semantics are documented on SSE run streaming; for workflow concepts and the distinct run-id identities, see Workflows & runs.
Failures and credits
There is no fixed per-node credit charge. Charging happens inline: LLM and agent calls meter token usage, and managed knowledge retrieval reserves and records credits per call. A mid-run hard stop — for example, exhausted credits — surfaces as a node error carrying a machine-readable reason code, so a client can match it deterministically. The managed-knowledge billing gate is best-effort inside a run: a billing hiccup is recorded but does not crash a workflow that is already running. For node-level retry behavior, how failures surface in the stream, and debugging failed runs, see Error handling & retries. For where and how managed usage is gated, see Usage gating & limits.Next steps
Node types overview
Every node type with its full parameter reference.
Variables & references
The complete
{{nodeId.path}} reference and run-state cookbook.Workflows & runs
What a run is, its status model, and the three run-id identities.
SSE run streaming
The full run-event taxonomy and how to consume it.