Flows
Learn how to define multi-step agent workflows using nodes and edges.
A flow is a directed graph of nodes connected by edges. It defines the execution order and data passing between steps in your workflow.
Flows are written in JSON or YAML following the Open Agent Specification. Field names in the file are snake_case. JSON and YAML are the two builtin input formats; a plugin can add another wire format without touching anything downstream of the parse.
Component references
Agent Spec files are disaggregated: components are defined once under $referenced_components, keyed by id, and referred to everywhere else by $component_ref.
nodes:
- $component_ref: start
- $component_ref: end
$referenced_components:
start:
component_type: StartNode
id: start
name: start
end:
component_type: EndNode
id: end
name: endThis matters because a flow refers to the same node from several places: start_node, nodes, and both edge lists. References keep those pointing at one shared component. Writing the same node out twice inline is rejected, because the parser compares component identity rather than names.
A complete flow
This flow takes a query, runs it through an agent with one tool, and returns a result:
component_type: Flow
agentspec_version: 26.2.0
name: research-flow
start_node:
$component_ref: start
nodes:
- $component_ref: start
- $component_ref: researcher
- $component_ref: end
control_flow_connections:
- component_type: ControlFlowEdge
name: start_to_researcher
from_node:
$component_ref: start
to_node:
$component_ref: researcher
- component_type: ControlFlowEdge
name: researcher_to_end
from_node:
$component_ref: researcher
to_node:
$component_ref: end
data_flow_connections:
- component_type: DataFlowEdge
name: query_to_researcher
source_node:
$component_ref: start
source_output: query
destination_node:
$component_ref: researcher
destination_input: query
- component_type: DataFlowEdge
name: result_to_end
source_node:
$component_ref: researcher
source_output: result
destination_node:
$component_ref: end
destination_input: result
$referenced_components:
start:
component_type: StartNode
id: start
name: start
outputs:
- title: query
type: string
researcher:
component_type: AgentNode
id: researcher
name: researcher
inputs:
- title: query
type: string
outputs:
- title: result
type: string
agent:
component_type: Agent
name: research-agent
system_prompt: |
You are a research assistant.
Answer the user's question: {{query}}
llm_config:
component_type: OpenAiConfig
name: openai
model_id: gpt-4o
tools:
- component_type: ServerTool
name: web_search
description: Search the web for information
inputs:
- title: query
type: string
outputs:
- title: results
type: string
end:
component_type: EndNode
id: end
name: end
inputs:
- title: result
type: stringid is optional, and a UUID is generated when you omit it, but any component you point at with $component_ref needs an explicit id to be addressable.
Nodes
Nodes are the building blocks of a flow. Every node has a component_type and a name:
| Type | What it does |
|---|---|
StartNode | Entry point; declares the flow's inputs |
EndNode | Exit point |
AgentNode | Runs an agent with tools |
LlmNode | Runs a prompt template through a model |
ToolNode | Executes an external tool directly |
BranchingNode | Routes execution on an input value |
See Nodes for detailed documentation on each type.
Properties
Node inputs and outputs are JSON Schema fragments carrying a title:
inputs:
- title: query
type: string
- title: max_results
type: integer
default: 10A title cannot be empty, and cannot contain spaces, quotes, braces, commas, or periods.
Edges
Control flow edges
control_flow_connections define execution order. Each edge names a from_node and a to_node, and optionally a from_branch:
control_flow_connections:
- component_type: ControlFlowEdge
name: start_to_agent
from_node:
$component_ref: start
from_branch: null
to_node:
$component_ref: agentfrom_branch: null, or leaving it out entirely, makes the edge unconditional. Only branch-producing nodes need named branches; see BranchingNode.
A node with several unconditional outgoing edges does not fan out in parallel. The first edge is taken and the others are ignored, so express alternatives with a BranchingNode.
Data flow edges
data_flow_connections map one node's output onto another node's input. source_output and destination_input are property titles:
data_flow_connections:
- component_type: DataFlowEdge
name: query_to_agent
source_node:
$component_ref: start
source_output: query
destination_node:
$component_ref: agent
destination_input: queryData flow edges currently add to the accumulated run state rather than isolating a node's inputs. Every node also sees the merged outputs of all nodes that ran before it, so identically-named outputs from different nodes overwrite one another. Declare your edges as though isolation were enforced, but avoid reusing one property title for two different meanings in the same flow.
Execution
When you run a flow, heddle:
- Parses the JSON/YAML file and resolves
$component_refreferences - Validates it against the Open Agent Specification schema
- Compiles the graph and validates its structure
- Walks from the
StartNode, one node at a time, until anEndNodeis reached - Prints the final state as JSON on stdout
Graph validation checks that every node is reachable from the start node, that a reachable EndNode exists, and that no non-terminal node is a dead end.
It also checks where a branch leads, which is otherwise something only the input that selects it reveals: a BranchingNode that maps a key to a branch no control flow edge leaves the node on is refused, as is an edge labelled with a branch its source never selects. Both would have ended a run mid-flight with no next node from "<node>".
Data flow edges are checked against what their source node can actually produce, where that is knowable. An LlmNode writes generated_text and nothing else, so an edge reading anything else from one is refused; the runner would have dropped the mapping silently and the destination would never have received the input. A source node the flow does not contain is refused for the same reason. Node types whose outputs are not fixed by heddle are not checked: an AgentNode writes result plus whatever keys its answer parsed to, a ToolNode writes what the tool returned, and a plugin node writes what its plugin returned.
Runner defaults
These are fixed and not currently configurable from the CLI:
| Setting | Default |
|---|---|
| Max nodes executed per run | 50 |
| Total run timeout | 5 minutes |
| Max tool rounds per agent | 10 |
| Tool execution timeout | 30 seconds |
Exceeding the node limit or the run timeout fails the run with an error.