Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.sirenspec.dev/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Workflow nodes let you execute another SirenSpec workflow inline as part of a parent workflow — enabling sub-workflow composition, code reuse, and modular workflow design. Any node with type: workflow is a workflow node.
nodes:
  run_b:
    type: workflow
    ref: ./workflows/b.yaml
    inputs:
      topic: "{{ extract.output }}"
The sub-workflow runs blocking (not in parallel) inside the parent workflow. Its output dict is written into the parent context so downstream nodes can reference it.

When to Use Workflow Nodes

Use workflow nodes when:
  • You want to compose modular, reusable workflow templates
  • A sub-workflow is shared across multiple parent workflows
  • You need to organize complex workflows into logical stages (e.g., extraction → analysis → synthesis)
  • You want to keep workflow files small and maintainable
Do NOT use workflow nodes when:
  • You want to run tasks in parallel (use swrm or factory instead)
  • You are passing the entire workflow context to a sub-workflow (inputs isolate the sub-workflow’s context by design)

Node Fields

FieldRequiredTypeDefaultDescription
typeYes"workflow"Node type discriminator. Must be "workflow".
refYesstringFile path or registry name for the sub-workflow.
inputsNodict[string, string]{}Template strings bound to the sub-workflow’s input context.
writesNostring"output.<node_id>"Dot-notation path where sub-workflow output is written.
max_depthNointeger10Maximum nesting depth before a ValidationError is raised.

ref — Sub-Workflow Reference

The ref field accepts two forms:

File Path

Relative or absolute paths to YAML workflow files, resolved at execution time:
nodes:
  run_child:
    type: workflow
    ref: ./workflows/b.yaml

  run_sibling:
    type: workflow
    ref: /absolute/path/to/workflow.yaml

Named Registry Entry

If a WorkflowRegistry is passed to execute(), ref can be a string name:
nodes:
  run_registered:
    type: workflow
    ref: child_workflow_name
The registry is passed programmatically:
from sirenspec import load_workflow, execute, WorkflowRegistry

registry = WorkflowRegistry()
registry.register("child_workflow_name", child_workflow)

workflow = load_workflow("parent.yaml")
trace = await execute(workflow, "hello", registry=registry)

inputs — Context Isolation

The sub-workflow’s context is initialized with only the keys declared in inputs. It does not inherit the parent’s full working context. This ensures clean separation of concerns: the sub-workflow only sees what the parent explicitly passes.
nodes:
  run_child:
    type: workflow
    ref: ./workflows/child.yaml
    inputs:
      topic: "{{ extract.output }}"
      max_length: "500"
Before the sub-workflow starts, template expressions in inputs are resolved against the parent’s context:
  • {{ extract.output }} — reads from the parent’s working or output
  • {{ env.API_KEY }} — reads environment variables
  • {{ inputs.message }} — reads the parent’s initial input
After resolution, the sub-workflow receives a clean context dict containing only the values in inputs. Inside the sub-workflow, these become available under {{ inputs.* }}:
# Inside child.yaml
nodes:
  analyze:
    agent: analyst
    writes: output.summary
    # Can reference {{ inputs.topic }} passed from parent

writes — Output Path

By default, sub-workflow output is written to output.<node_id>. You can customize this path:
nodes:
  run_child:
    type: workflow
    ref: ./child.yaml
    writes: output.analysis  # custom path
Without writes, output is written to output.run_child:
edges:
  - from: run_child
    to: next_step
    when: output.run_child != null
With writes: output.analysis, output is written to output.analysis:
edges:
  - from: run_child
    to: next_step
    when: output.analysis != null
The sub-workflow’s output dict (keyed by sub-node ID) is stored at the specified path, so you can reference individual sub-nodes:
# Parent workflow
nodes:
  summarize:
    agent: summarizer
    writes: output.final
    # In system prompt, reference sub-node outputs:
    # {{ output.run_child.extract }} — first sub-node's output
    # {{ output.run_child.analyze }} — second sub-node's output

max_depth — Nesting Limit

Workflow nodes can nest arbitrarily deep, but to prevent infinite recursion cycles, each node has a nesting depth limit (default: 10).
nodes:
  run_child:
    type: workflow
    ref: ./child.yaml
    max_depth: 5  # raise ValidationError if nesting exceeds 5 levels
If a sub-workflow node is executed at depth >= max_depth, execution raises ValidationError:
Max workflow nesting depth 5 exceeded for node 'run_child'

Output Shape

After a workflow node executes, its sub-workflow’s output dict is written to the parent context:
Context pathValue
output.<node_id> (or custom writes path)The sub-workflow’s output dict (keys are sub-node IDs).
working.<node_id>.sub_workflow_traceThe sub-workflow’s full execution trace.

Accessing Sub-Node Outputs

Reference individual sub-node outputs via the output dict:
nodes:
  downstream:
    agent: processor
    writes: output.result
    # Downstream node can access sub-workflow outputs:
    # {{ output.run_child.extract }} — output of sub-node named "extract"
    # {{ output.run_child.analyze }} — output of sub-node named "analyze"

Complete Example

Parent workflow (parent.yaml):
version: "0.1"

agents:
  triage:
    model: "openai:gpt-4o-mini"
    system: |
      Classify the user input as either a refund request or a general inquiry.
      Reply with only "refund" or "general".

  reporter:
    model: "openai:gpt-4o-mini"
    system: |
      Based on the analysis results, produce a concise summary.

nodes:
  classify:
    agent: triage
    writes: working.intent

  run_analysis:
    type: workflow
    ref: ./analysis.yaml
    inputs:
      message: "{{ inputs.message }}"
      intent: "{{ working.intent }}"
    writes: output.analysis

  report:
    agent: reporter
    writes: output.final
    # Can reference sub-workflow outputs:
    # {{ output.analysis.sentiment }} — sentiment sub-node's output
    # {{ output.analysis.risk }} — risk sub-node's output

edges:
  - from: classify
    to: run_analysis
  - from: run_analysis
    to: report
Sub-workflow (analysis.yaml):
version: "0.1"

agents:
  sentiment_agent:
    model: "openai:gpt-4o-mini"
    system: "Analyze sentiment in: {{ inputs.message }}"

  risk_agent:
    model: "anthropic:claude-haiku-4-5-20251001"
    system: "Identify risks in: {{ inputs.message }}"

nodes:
  sentiment:
    agent: sentiment_agent
    writes: output.sentiment

  risk:
    agent: risk_agent
    writes: output.risk

edges:
  - from: sentiment
    to: risk
Run it:
sirenspec run parent.yaml --input "I'd like to request a refund."
The parent’s output will include:
{
  "analysis": {
    "sentiment": "The user is unhappy and frustrated...",
    "risk": "High risk of customer churn if not handled well..."
  },
  "final": "Summary based on analysis..."
}

Error Handling

ValidationError: Max workflow nesting depth exceeded

Raised when a workflow node is executed at a depth >= its max_depth. Increase max_depth or simplify your workflow nesting:
nodes:
  run_child:
    type: workflow
    ref: ./child.yaml
    max_depth: 15  # increase if needed

FileNotFoundError

Raised when ref points to a non-existent file:
FileNotFoundError: ./workflows/child.yaml not found
Check the file path relative to the parent workflow file location.

KeyError

Raised when ref is a named string and the registry does not contain that workflow:
KeyError: Workflow 'missing_name' not found in WorkflowRegistry
Register the workflow before execution:
registry.register("missing_name", workflow)

Template Interpolation

In inputs values, you can use all standard SirenSpec template syntax:
PlaceholderDescription
{{ inputs.message }}The parent’s initial input message.
{{ working.* }}Any value written to the parent’s working context.
{{ output.* }}Any value written to the parent’s output context.
{{ env.VAR_NAME }}Environment variables.
nodes:
  run_child:
    type: workflow
    ref: ./child.yaml
    inputs:
      report: "{{ working.extracted_report }}"
      threshold: "{{ env.THRESHOLD }}"
      user_msg: "{{ inputs.message }}"

JSON Schema

The sirenspec.schema.json artifact validates workflow nodes inline with other node types. IDEs that support JSON Schema (VS Code, JetBrains) will autocomplete type: workflow, ref, and all fields automatically.