heddle

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: end

This 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: string

id 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:

TypeWhat it does
StartNodeEntry point; declares the flow's inputs
EndNodeExit point
AgentNodeRuns an agent with tools
LlmNodeRuns a prompt template through a model
ToolNodeExecutes an external tool directly
BranchingNodeRoutes 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: 10

A 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: agent

from_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: query

Data 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:

  1. Parses the JSON/YAML file and resolves $component_ref references
  2. Validates it against the Open Agent Specification schema
  3. Compiles the graph and validates its structure
  4. Walks from the StartNode, one node at a time, until an EndNode is reached
  5. 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:

SettingDefault
Max nodes executed per run50
Total run timeout5 minutes
Max tool rounds per agent10
Tool execution timeout30 seconds

Exceeding the node limit or the run timeout fails the run with an error.