> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dagraph.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Control workflow costs with budgets and retry policies

> Set hard token and USD caps at the DAG or node level to prevent cost runaway, and configure per-node retry policies to handle transient provider errors.

dagraph gives you two independent mechanisms to keep workflows safe: budget caps that stop a run the moment it would exceed your spending limit, and retry policies that automatically recover from transient provider errors like rate limits or network timeouts. Both work at the DAG level (applying to the whole run) and at the node level (applying to a single node's total spend or failure handling).

## Budget caps

### DAG-level budget

Set a `budget:` field at the top of your YAML to apply a global cap to the entire run. If the cap is exceeded at any point — mid-node, mid-iteration — dagraph raises `BudgetExceededError` and stops immediately. No additional charges accumulate after the cap is hit.

```yaml theme={null}
name: research
budget:
  max_tokens: 50000
  max_usd: 2.00

nodes:
  - id: researcher
    type: agent
    model: claude-haiku-4-5-20251001
    prompt: "Research {{ topic }}."
```

<ParamField path="budget.max_tokens" type="integer">
  Maximum billable tokens for the run. Billable tokens are input tokens plus output tokens. Cache read and cache write tokens are excluded — this matters on the `claude_code` backend, which loads a large system-prompt cache for each subprocess. That overhead does not count against your token cap.
</ParamField>

<ParamField path="budget.max_usd" type="number">
  Maximum spend in US dollars. The engine calculates cost using published per-million-token prices for each model. You can set `max_tokens`, `max_usd`, or both — each is checked independently.
</ParamField>

<Warning>
  The budget cap is a hard stop. When `BudgetExceededError` is raised, the run ends immediately. Completed node outputs are preserved in the artifact store, but the run cannot be resumed. Design your DAG-level caps with enough headroom that normal runs stay well under the limit.
</Warning>

### Per-node budget

Override the budget for a single node by adding a `budget:` field directly on the node. This is useful when one expensive node (such as a synthesizer using `claude-opus-4-7`) should be capped independently while lighter nodes share the DAG-level budget.

A per-node cap covers the total spend of everything that node does — for an `evaluator_loop` with three iterations, the cap applies across all six LLM calls (three generator + three evaluator calls combined).

```yaml theme={null}
budget:
  max_tokens: 100000
  max_usd: 5.00

nodes:
  - id: cheap_researcher
    type: agent
    model: claude-haiku-4-5-20251001
    prompt: "Summarize {{ topic }} briefly."

  - id: expensive_synthesizer
    type: agent
    model: claude-opus-4-7
    depends_on: [cheap_researcher]
    budget:
      max_usd: 1.50       # This node alone won't exceed $1.50
    prompt: "Write a detailed report based on: {{ cheap_researcher }}"
```

<Note>
  If you use `claude_code` as your backend, the USD cost shown is an approximation based on the API pricing table. The `claude_code` backend draws from your Claude Code subscription plan, not your API balance. Use `max_tokens` for a more reliable cap on that backend.
</Note>

## Retry policies

### Configuring retries

Add a `retry:` field to any node to automatically re-run it on failure. Without `retry:`, every node runs exactly once.

```yaml theme={null}
- id: fetch_summary
  type: agent
  model: claude-sonnet-4-6
  retry:
    max_attempts: 3
    backoff_seconds: 5
    retry_on: ["*"]
  prompt: "Summarize the latest news about {{ topic }}."
```

<ParamField path="retry.max_attempts" type="integer" default="1">
  Total number of attempts including the first. Minimum 1, maximum 20. `max_attempts: 3` means one initial attempt and up to two retries.
</ParamField>

<ParamField path="retry.backoff_seconds" type="number" default="0">
  Seconds to wait between attempts. Use a non-zero value when retrying after rate-limit errors so you don't immediately hit the same limit again.
</ParamField>

<ParamField path="retry.retry_on" type="string[]" default="[&#x22;*&#x22;]">
  A list of exception class names to retry on. `"*"` matches any exception. Names are matched against the full class hierarchy, so `["OSError"]` also catches `FileNotFoundError`. Narrow this list to avoid retrying on errors that indicate bad inputs or invalid prompts.
</ParamField>

### Exceptions that are never retried

Two exceptions bypass `retry_on` entirely and are never retried, regardless of your configuration:

* **`BudgetExceededError`** — the budget cap is a hard limit; retrying would immediately exceed it again.
* **`ApprovalPending`** — this is a control-flow signal from an `approval_gate` node, not a failure.

<Tip>
  For transient provider errors (rate limits, network timeouts, intermittent 5xx responses), use `retry_on: ["*"]` with `max_attempts: 3` and `backoff_seconds: 5`. This covers the common case without you needing to know the exact exception class name.
</Tip>

## Conditional execution

Use the `when:` field on any node to skip it entirely based on inputs or upstream outputs. The expression is a Jinja template evaluated against all inputs and the outputs of completed upstream nodes. If the expression is truthy, the node runs normally. If falsy, the node is skipped — its status is set to `skipped` and an empty string is passed downstream.

```yaml theme={null}
nodes:
  - id: check_topic
    type: agent
    model: claude-haiku-4-5-20251001
    prompt: "Is '{{ topic }}' a valid research topic? Reply yes or no."

  - id: deep_research
    type: agent
    model: claude-sonnet-4-6
    depends_on: [check_topic]
    when: "{{ topic | length > 10 }}"
    prompt: "Research {{ topic }} in depth."
```

Skipped nodes are not retried and do not consume budget. Downstream nodes that depend on a skipped node still run — they just receive an empty string for that node's output variable.

## Putting it all together

The following example combines a DAG-level budget, a per-node budget override, and a retry policy on the node most likely to hit rate limits:

```yaml theme={null}
name: resilient_research
description: Research with cost controls and automatic retries.

budget:
  max_tokens: 60000
  max_usd: 3.00

nodes:
  - id: research_a
    type: agent
    model: claude-haiku-4-5-20251001
    retry:
      max_attempts: 3
      backoff_seconds: 5
      retry_on: ["*"]
    prompt: "Research '{{ topic }}' — technical angle. 5-8 bullets."

  - id: research_b
    type: agent
    model: claude-haiku-4-5-20251001
    retry:
      max_attempts: 3
      backoff_seconds: 5
      retry_on: ["*"]
    prompt: "Research '{{ topic }}' — economic angle. 5-8 bullets."

  - id: synthesizer
    type: agent
    model: claude-sonnet-4-6
    depends_on: [research_a, research_b]
    budget:
      max_usd: 1.00
    prompt: |
      Synthesize these research findings into a unified report.
      Technical: {{ research_a }}
      Economic: {{ research_b }}
```
