> ## Documentation Index
> Fetch the complete documentation index at: https://graph-unify-model-roles.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# config.toml

Config is layered TOML — later wins, tables deep-merge:

1. `~/.config/graph/config.toml` (global)
2. `./.graph/config.toml` (project)
3. Environment variables (`GRAPH_STORAGE`, `GRAPH_LOG`)
4. CLI flags

`${VAR}` in any string value resolves from the environment at load time. An unset variable never silently becomes an empty string — but *when* it errors depends on where it sits:

* **Inside `[providers.*]` or `[mcp.*]`** the error is deferred to the moment that provider or server is actually used: the value keeps its literal `${VAR}` text, everything that doesn't need the secret (plan authoring, listing, `graph mcp serve`, key-free plans) keeps working, and the first call that does need it fails naming the variable and the config path that references it.
* **Anywhere else** (paths, prompts, settings) an unset variable still fails the load immediately — a `data_dir` carrying literal `${VAR}` text would be silently wrong everywhere.

## `version`

```toml theme={null}
version = 1
```

The [file version](/reference/file-versions#config) the config is written in; omitted means `1`. Each layer is migrated to the current version on its own before the two merge, so the global and project files may sit at different versions. `graph config check` reports each file's version, and `graph config migrate` (or `--global`) rewrites a file to the current one with its comments intact. A file outside the binary's support window is refused by name, with the version it found and the versions the binary reads.

## `[settings]`

```toml theme={null}
[settings]
data_dir = "~/.local/share/graph"   # where threads and the shape cache live
max_agent_iterations = 15           # agent-loop tool rounds per turn
planning_attempts = 2               # plan_and_execute replan budget
```

## `[providers.*]`

```toml theme={null}
[providers.anthropic]
type = "anthropic"                  # anthropic | openai | openai_compat | bedrock
api_key = "${ANTHROPIC_API_KEY}"

[providers.local]
type = "openai_compat"
base_url = "http://localhost:11434/v1"    # Ollama, vLLM, LM Studio…

[providers.bedrock]                 # roadmap
type = "bedrock"
region = "us-east-1"
profile = "default"
```

Provider behavior — structured-output mechanisms, retries, and the `bedrock` roadmap status — is covered in [Models & providers](/models/models-and-providers#providers).

## `[models.<role>]` — model roles

Every model is a **role**: one `[models.<role>]` table per name, all with the same keys. The standard roles are the ones graph's own calls resolve through, falling back to `default`; any other name is a custom role.

```toml theme={null}
[models.default]
provider = "anthropic"
model = "claude-sonnet-5"

[models.solver]
provider = "anthropic"
model = "claude-haiku-4-5"
temperature = 0.4

[models.nano]
provider = "anthropic"
model = "claude-haiku-4-5"
description = "fast and cheap; small self-contained tasks like per-item map bodies"
```

| Key           | Required | Meaning                                                                                                   |
| ------------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `provider`    | yes      | a `[providers.<name>]` entry                                                                              |
| `model`       | yes      | the model id sent to that provider                                                                        |
| `temperature` | no       | request temperature; provider default when unset                                                          |
| `description` | no       | what the role is good for — a planner-facing routing signal; a role with one is advertised to the planner |
| `fallbacks`   | no       | ordered failover candidates, below                                                                        |

| Standard role | Used for                                                     |
| ------------- | ------------------------------------------------------------ |
| `default`     | what every standard role falls back to                       |
| `chat`        | the agent loop (`ask`/`chat`)                                |
| `planner`     | `plan_and_execute` plan authoring                            |
| `solver`      | plan report synthesis                                        |
| `repair`      | one-shot JSON repair when structured output fails to parse   |
| `judge`       | yes/no verdicts for [inferred exit gates](/plans/exit-gates) |

Custom roles are selectable by name from a [prompt tool](/tools/user-defined)'s `model` field, [`builtin__infer`](/tools/builtins#builtin-infer)'s `model` input, and an `infer` gate's `model` override; they never fall back. Where each role fires, the usual cost setup, and the selection rules are in [Models & providers](/models/models-and-providers#roles).

### `fallbacks` — provider failover

Any role can carry an ordered list of failover candidates, used when its provider is having an outage:

```toml theme={null}
[models.chat]
provider = "anthropic"
model = "claude-sonnet-5"
fallbacks = [
    { provider = "openai", model = "gpt-5" },
    { provider = "local", model = "llama3", temperature = 0.3 },
]
```

Each candidate names its own provider **and** model; `temperature` optionally overrides. Every referenced provider must exist under `[providers]` (checked at startup). When failover triggers, what carries over, and where fallbacks apply are in [Models & providers](/models/models-and-providers#provider-failover).

## `[mcp.*]`

See [MCP servers](/tools/mcp-servers) for full detail.

```toml theme={null}
[mcp.<name>]
command = "…"            # stdio — XOR with url
args = ["…"]
env = { KEY = "${VAR}" }
url = "https://…"        # streamable HTTP
headers = { Authorization = "Bearer ${VAR}" }
include_tools = ["…"]    # optional allowlist
exclude_tools = ["…"]    # optional blocklist

[mcp.<name>.tool_overrides.<tool>]
description = "…"
output_schema = { … }    # declare shapes the server omits
output_example = { … }   # optional worked example, shown to the planner
```

## `[plans]` and `[tools]`

```toml theme={null}
[plans]
paths = ["./.graph/plans", "~/.config/graph/plans"]

[tools]
paths = ["./.graph/tools", "~/.config/graph/tools"]
packs = []              # bundled tool packs to enable, e.g. ["github", "slack"]
```

`packs` enables the opt-in [built-in tool packs](/tools/builtins) — tool definitions that ship inside the binary, served under the `builtin__` namespace. The `llm` and `data` packs are always on; `packs` adds the rest (`github`, `slack`).

## `[pricing]` — per-model token prices

What each model costs, in **USD per million tokens**, keyed by the model id exactly as written in `[models]` — that string is what goes on the wire.

```toml theme={null}
[pricing."claude-sonnet-5"]
input = 3.00
output = 15.00
# cache_write = 3.75   # optional; defaults to input × 1.25
# cache_read = 0.30    # optional; defaults to input × 0.10

[pricing."claude-haiku-4-5"]
input = 1.00
output = 5.00
```

The section is optional and has **no built-in defaults**. Without it a run still reports its token counts; it just omits `cost_usd` rather than guessing a rate. That is deliberate — published prices change, and a stale table that quietly reports the wrong dollar figure is worse than reporting none. Price only the models you care about: an unpriced model contributes tokens to the report and nothing to the cost.

`cache_write` and `cache_read` default to the standard 1.25× and 0.10× multipliers on `input`. Set them when a provider prices caching differently.

Where the numbers surface: [what a run spent](/reference/scripting-contract#what-a-run-spent).

## `[storage]`

```toml theme={null}
[storage]
backend = "file"        # default: plain files under data_dir
# backend = "memory"    # ephemeral (CI); or GRAPH_STORAGE=memory
```

## `[user]`

Injected into the agent's and planner's context:

```toml theme={null}
[user]
name = "Tyler"
context = "CEO and technical architect of LaunchNotes. Primary Linear team: LaunchNotes."
timezone = "America/Chicago"
```

## `[prompts]`

System-prompt overrides. Each field **replaces** the built-in text wholesale; leave a field unset (or delete it) to keep the default. For additive context, use `[user].context` instead.

`graph config init` writes this section out pre-filled with the built-in defaults, so the usual starting point is editing those in place. A field left in the file pins that prompt at the written text — delete it to track the shipped default across releases.

```toml theme={null}
[prompts]
# Base system prompt for the chat/ask agent loop. The current date/time
# and the [user] name/context are still appended after it.
chat = """
You are graph, a command-line assistant for release engineering.
Lead with the answer; keep formatting terminal-friendly.
"""

# Framing and policy for the agent inside `graph workbench`. The
# workbench__* tool rules are appended after it either way.
workbench = """
# Plan workbench
Build the draft plan with the user. Run and save only when asked.
"""
```

The `workbench` override covers framing and policy only — how the agent should behave in the workbench, when it may run or save, whether it reads the project. The rules describing what each `workbench__*` tool does to the draft are appended after it and cannot be overridden, so an override can change the agent's manners without breaking its grip on the draft. Every tool also carries its own description, which the override never replaces.

## `[workbench]`

```toml theme={null}
[workbench]
log_path = "~/logs/graph-workbench.log"   # default: <data_dir>/workbench.log
```

Where the [workbench's debug log](/workbench/plan-workbench#debug-logging) is written (tilde-expanded). The `GRAPH_WORKBENCH_LOG` environment variable overrides both.
