Nodes
Reference for all node types available in heddle workflows.
Nodes are the building blocks of a flow. Each node performs one step of your workflow.
The examples below are single components, as they appear under a flow's $referenced_components. See Flows for how they are wired together.
heddle runs six of Agent Spec's fourteen node types. StartNode, EndNode,
AgentNode, LlmNode, ToolNode and BranchingNode execute. ApiNode,
CatchExceptionNode, FlowNode, InputMessageNode, MapNode, OutputMessageNode,
ParallelFlowNode and ParallelMapNode are valid Agent Spec, deserialise cleanly, and
are then refused by name when the flow is parsed. A spec that is correct by the
specification can still fail here.
A seventh possibility is a node type a plugin registers. It is written into nodes[] like
any other, brings its own executor, and declares its own inputs, outputs and branches.
See Plugins.
StartNode
The entry point of a flow. Its outputs document what the flow expects to be given at
run time; they do not enforce it. --input becomes the starting state whatever it
contains: a key nothing declares is carried, and a declared key you omit is absent
rather than defaulted.
component_type: StartNode
name: start
outputs:
- title: query
type: string
- title: max_results
type: integer
default: 10A flow must have exactly one StartNode.
EndNode
The exit point of a flow. Its declared inputs filter nothing: an EndNode passes state
through, so the run returns the whole accumulated state that reached it, merged with
anything a data-flow edge mapped in. Declare them to document the shape you intend.
branch_name is accepted and read nowhere. In Agent Spec it feeds branch inference for
subflow nodes, which heddle does not support, so labelling an EndNode with it changes
nothing about how the flow runs.
component_type: EndNode
name: end
inputs:
- title: result
type: string
branch_name: nextReaching any EndNode ends the run, and the state at that point is printed as JSON.
AgentNode
An LLM-powered agent that can call tools autonomously. The node sends the prompt to the LLM, the LLM decides which tools to call, the tools execute, and their results feed back to the LLM until it answers without requesting more tools.
component_type: AgentNode
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: stringThe nested agent carries the prompt, the LLM config, and the tool declarations. system_prompt supports {{variable}} substitution from the node's input state.
The agent runs at most 10 rounds of tool calling; exceeding that fails the run.
A tool input carrying a default is optional: it is left out of the schema's required
list, and if the model omits it the default is filled in before the tool runs. An input
without a default is required.
What the model is sent. heddle builds a system message from the system_prompt, with
{{key}} placeholders substituted from the run's state, and then a user message carrying
the node's entire input state as JSON. The agent therefore sees its inputs whether or not
the prompt mentions them.
transforms attaches guardrails to the agent; see
Plugins. They change this node's contract in ways worth knowing: a pre
transform that rejects skips the model call entirely and the node returns
transform_status: "rejected" instead of its usual output, and an agent carrying a post
transform does not stream, because a token that has reached a client cannot be recalled
once the transform rejects the answer.
Agent Spec defines human_in_the_loop, toolboxes and requires_confirmation on an
agent. heddle parses all three and acts on none. human_in_the_loop defaults to true in
the schema, so a spec that appears to ask for human approval gets none.
The node's output is the model's final message under the key result. If that message is itself a JSON object, its keys are merged into the output as well.
LlmNode
Runs a prompt template through an LLM, with no tool-calling loop. prompt_template supports {{variable}} substitution from the node's input state.
component_type: LlmNode
name: summarizer
prompt_template: |
Summarize the following text in three bullet points:
{{text}}
llm_config:
component_type: OpenAiConfig
name: openai
model_id: gpt-4o
inputs:
- title: text
type: string
outputs:
- title: generated_text
type: stringThe response is written to the output key generated_text.
Substitution only fills in string values. A placeholder bound to a number, object, or array is replaced with an empty string, and a placeholder with no matching key is left in the prompt verbatim.
ToolNode
Executes an external tool directly, with no LLM involved. The node's input state is passed to the tool as JSON, and the tool's JSON output becomes the node's output.
component_type: ToolNode
name: fetch_data
inputs:
- title: url
type: string
outputs:
- title: body
type: string
tool:
component_type: ServerTool
name: fetch_api
description: Fetch a URL and return the response body
inputs:
- title: url
type: string
outputs:
- title: body
type: stringThe tool's name must resolve in the flow's tool registry: an executable in
--tools-dir, or a tool a loaded plugin contributes, which exists nowhere on disk. See
Tools.
BranchingNode
Routes execution to a named branch by looking up an input value in mapping. Mapping keys are input values; mapping values are branch names, which must match the from_branch of an outgoing control flow edge.
component_type: BranchingNode
name: router
inputs:
- title: branching_mapping_key
type: string
mapping:
tech: tech_branch
science: science_branch
DEFAULT_BRANCH: default_branchThe value is read from the input named branching_mapping_key. Wire it with a data flow edge:
data_flow_connections:
- component_type: DataFlowEdge
name: category_to_router
source_node:
$component_ref: classifier
source_output: category
destination_node:
$component_ref: router
destination_input: branching_mapping_keyUnmatched values fall back in two steps: to the branch named by the special DEFAULT_BRANCH mapping key if you define one, and otherwise to the branch literally named default. If neither exists as an outgoing edge, the run fails with a "no next node" error, so give every branching node a default path.
If branching_mapping_key is missing from the input state entirely, the node falls back to the first string value it finds in that state, which makes routing depend on key ordering. Always wire branching_mapping_key explicitly.