heddle

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

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

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

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

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

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

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

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