Skip to main content
Not every step in a workflow needs an LLM. Sometimes you need to run a shell command to collect system information, post-process an agent’s JSON output with a Python script, or call an external tool that doesn’t have an API wrapper. dagraph’s bash and python_exec node types let you mix code execution with LLM calls in the same DAG, with stdout becoming the node’s artifact that downstream nodes can reference just like any agent output.

How exec nodes fit into a DAG

Exec nodes follow the same dependency model as agent nodes. Their output (stdout) is stored as an artifact and referenced by downstream nodes via {{ node_id }}. A non-zero exit code fails the node and halts any downstream nodes that depend on it. The code_and_llm.yaml example collects system information with bash, parses it with Python, then summarizes it with an LLM:
name: code_plus_llm
description: |
  Mix exec nodes with LLM nodes. A bash node collects system info, a
  python_exec node post-processes it, then an agent summarizes it in prose.

budget:
  max_tokens: 10000
  max_usd: 1.00

nodes:
  # Wave 1: collect raw system data
  - id: collect
    type: bash
    command: |
      echo "hostname: $(hostname)"
      echo "python: $(python3 --version 2>&1)"
      echo "uptime: $(uptime | tr -s ' ')"
    timeout_seconds: 10

  # Wave 2: parse the raw text into structured JSON
  - id: parse
    type: python_exec
    depends_on: [collect]
    timeout_seconds: 10
    code: |
      import json
      {% raw %}lines = {{ collect | tojson }}.strip().split("\n"){% endraw %}
      result = {line.split(":", 1)[0].strip(): line.split(":", 1)[1].strip()
                for line in lines if ":" in line}
      print(json.dumps(result, indent=2))

  # Wave 3: LLM summarizes the structured data
  - id: narrate
    type: agent
    model: claude-haiku-4-5-20251001
    max_output_tokens: 300
    depends_on: [parse]
    prompt: |
      Summarize this system-info JSON in 2–3 plain-English sentences
      for a non-technical reader:

      {{ parse }}
Run it:
agentgraph run code_and_llm.yaml --sandbox inprocess
Any DAG that contains bash or python_exec nodes requires the --sandbox flag. Without it, dagraph refuses to run the DAG to prevent accidental code execution.

The bash node

Use bash to run shell commands. The command field is a Jinja template — you can interpolate DAG inputs and upstream node outputs just like in an agent prompt.
- id: fetch_prices
  type: bash
  command: |
    curl -s "https://api.example.com/prices?symbol={{ symbol }}" \
      -H "Authorization: Bearer ${API_TOKEN}"
  timeout_seconds: 30
  env:
    API_TOKEN: "${API_TOKEN}"    # passed from the environment
Key fields:
FieldDefaultDescription
commandrequiredShell command, Jinja-rendered against inputs and dep outputs
timeout_seconds300Fail the node if execution takes longer
env{}Additional environment variables for the process
imagenullDocker image to use (docker sandbox only)
max_output_bytes1,000,000Truncate stdout beyond this limit

The python_exec node

Use python_exec to run Python code inline. The code field is also Jinja-rendered, which means curly braces in Python code (f-strings, dict literals, format strings) need to be wrapped in {% raw %}...{% endraw %} to prevent Jinja from interpreting them:
- id: parse
  type: python_exec
  depends_on: [collect]
  code: |
    import json
    {% raw %}
    data = {{ collect | tojson }}
    parsed = json.loads(data)
    summary = {k: v for k, v in parsed.items() if v}
    print(json.dumps(summary, indent=2))
    {% endraw %}
Without {% raw %}, Jinja sees {{ collect | tojson }} as a template expression (which is correct), but it also tries to interpret {k: v for k, v in parsed.items()} as a template block and fails.
The print() call is how your Python code returns its result. Whatever you print to stdout becomes the node’s artifact. Print a JSON string if downstream nodes or agents need structured data.

Sandbox backends

dagraph supports two sandbox backends, selected via --sandbox:
Runs the command or script as a child process of the dagraph scheduler. Fast and simple — no Docker required.
agentgraph run code_and_llm.yaml --sandbox inprocess
Use inprocess when you own the code being executed — your own scripts, trusted utilities, or commands you’ve written yourself.
Use --sandbox docker for any code you did not write yourself or that comes from user-controlled input. The inprocess sandbox runs code in the same process space as dagraph, so malicious code can access environment variables, API keys, and the filesystem. Docker containers are isolated by default.

Output size limit

Both bash and python_exec nodes default to a 1 MB stdout limit. If a node’s output exceeds max_output_bytes, dagraph truncates it and fails the node with an error. Override the limit per node if you expect large outputs:
- id: large_export
  type: bash
  command: "cat large_dataset.json"
  max_output_bytes: 10_000_000   # allow up to 10 MB
Downstream agent nodes receive this text in their context window — keep outputs as compact as possible to avoid inflating token costs.

Passing exec output to an LLM

Exec node output is available in downstream agent prompts via {{ node_id }} — exactly like any other node:
- id: summarize
  type: agent
  model: claude-haiku-4-5-20251001
  depends_on: [parse]
  prompt: |
    Here is a JSON summary of the system:
    {{ parse }}

    Explain what it shows in one paragraph.

Parallel agents

Run exec nodes and agent nodes in parallel waves across the same DAG.

Multi-provider fallback

Add fallback chains to the agent nodes downstream of your exec steps.