Config
substructure.toml declares one project. Every key is here.
The CLI reads the file from the working directory, or from the path you pass
with -c. It does not search parent directories.
An unknown key is a parse error.
A full file
name = "support-bot"
db = "substructure.db"
log = "info"
[llm.claude]
type = "anthropic"
[agent.support]
llm = "claude"
model = "claude-sonnet-4-5"
system = "You are a support agent."
mcp = ["sentry"]
sub_agents = ["researcher"]
[agent.researcher]
description = "Finds and reads sources."
llm = "claude"
model = "claude-haiku-4-5"
[mcp.sentry]
url = "https://mcp.sentry.dev/mcp"
[slack]
dm = "support"
mentions = "support"
[run]
agent = "support"
output = "pretty"
[serve]
host = "127.0.0.1"
port = 8080
[remote]
url = "https://api.substructure.ai"
org = "org_01hx…"
project = "proj_01hx…"Two roles
A file has two roles. It can have one or both.
| Role | Keys | Commands |
|---|---|---|
| An engine you run | db, log, [run], [serve] | subs run, subs serve |
| A deployment you administer | [remote] | subs apply, subs keys, subs sessions |
What the project is stays the same for both roles: name, [llm.<id>],
[agent.<id>], [mcp.<id>], and [slack].
A second environment is a second file. subs apply -c substructure.staging.toml
deploys a separate project.
Top level
| Key | Type | Default | Meaning |
|---|---|---|---|
name | string | none | The project's name. subs apply creates the project from it and renames it when it changes. |
db | path | the file's name with a .db suffix | The SQLite file holding events, sessions, and connector credentials. A relative path resolves against the file. |
log | string | error for run, info for serve | A RUST_LOG filter. $RUST_LOG wins over it. |
substructure.toml uses substructure.db. subs.staging.toml uses
subs.staging.db. Two files in one directory are two engines.
[llm.<id>]
Where a model call runs. An agent names a block by its id.
[llm.claude]
type = "anthropic"
api_key_env = "MY_ANTHROPIC_KEY"
[llm.byo]
type = "worker"
format = "anthropic"| Key | Type | Default | Meaning |
|---|---|---|---|
type | anthropic, openai, openrouter, worker | required | Who makes the call. |
api_key_env | string | the vendor's own variable | The variable holding the key. For an engine you run. |
base_url | url | the vendor's own | Where to send the call. |
format | openai, anthropic | the engine's own shape | The wire shape of the llm.execute a worker answers. type = "worker" only. |
There is no default block and no fallback. An agent names a block, or its calls fail. See LLMs.
api_key_env names a variable on your machine. subs apply removes it. A
deployment refuses a document that carries one.
[agent.<id>]
An agent. The id is what clients, channels, and parent agents route on.
[agent.support]
llm = "claude"
model = "claude-sonnet-4-5"
system = "You are a support agent."
mcp = ["sentry"]
sub_agents = ["researcher"]
tools = [{ name = "confirm", description = "Ask the human", handler = "client" }]
worker = "https://bot.example.com/agent"
signing_secret_env = "SUPPORT_SIGNING_SECRET"
[agent.support.retry]
tool = { max_attempts = 3 }| Key | Type | Meaning |
|---|---|---|
llm | string | The [llm.<id>] block. Required when the section sets anything. |
model | string | The model. Required when the section sets anything. |
system | string | The system prompt. |
description | string | What this agent does, shown to a parent that calls it. |
mcp | list | Connections. An id, or { id, tools } to take fewer tools. |
sub_agents | list of ids | Agents this one can call. |
tools | list | Browser tools. Each needs handler = "client". |
worker | url | Where decisions go. Leave it off and the engine decides. |
signing_secret_env | string | The variable holding the signing secret. For an engine you run. |
retry | table | Timeouts and attempts, per kind. See Retries. |
An agent that sets nothing needs a worker. An agent that sets anything needs
llm and model. See Agents.
The tools your worker runs are worker code. They do not go in the file.
[mcp.<id>]
An MCP server the engine connects to.
[mcp.sentry]
url = "https://mcp.sentry.dev/mcp"
[mcp.github]
url = "https://api.githubcopilot.com/mcp/"
auth = { token_env = "GITHUB_TOKEN" }
prefix_tools = false| Key | Type | Default | Meaning |
|---|---|---|---|
url | url | required | The server. |
auth | table | none | { token_env = "…" } for a static credential. For an engine you run. |
prefix_tools | bool | true | Show the model <id>__<tool> instead of the connection's own names. |
A token written in the file is a parse error. Authorize a server with
subs mcp login <id>. See Connectors.
[slack]
Where the bot answers. Every key defaults to silence.
[slack]
dm = "support"
mentions = "support"
[slack.channel.C0ENGOPS]
agent = "oncall"
[slack.channel.C0RANDOM]
off = true| Key | Type | Meaning |
|---|---|---|
dm | agent id | Answers direct messages. |
mentions | agent id | Answers mentions in any channel channel does not name. |
channel.<id>.agent | agent id | Answers in that channel. |
channel.<id>.off | bool | The bot stays out of that channel. |
Name a channel by id, never by name. A #name is a parse error. See
Slack.
[run]
Defaults for subs run.
| Key | Type | Default | Meaning |
|---|---|---|---|
agent | agent id | none | Which agent a bare subs run uses. |
output | ag-ui, jsonl, pretty | ag-ui | How to print the turn. |
[serve]
Defaults for subs serve.
| Key | Type | Default | Meaning |
|---|---|---|---|
host | string | 127.0.0.1 | The address to bind. |
port | number | 8080 | The port. |
auth | bool | true | Client and worker authentication. Set false only for a server nothing off this machine can reach. |
[remote]
The deployment this file administers. That can be the hosted cloud, one you
host, or another person's subs serve.
| Key | Type | Default | Meaning |
|---|---|---|---|
url | url | https://api.substructure.ai | The API to talk to. |
org | id | none | Written by subs link or subs apply. |
project | id | none | Written by subs apply when it creates the project. |
subs apply writes the pin back into the file and keeps your comments. A second
apply changes nothing.
Precedence
flag > environment variable > substructure.toml > default
Setting a value in the file still lets you override it on the command line.
The message, --input, and --session have no key in the file. They say what
one run does.
Secrets
The file names secrets. It never holds them.
| Secret | How the file refers to it |
|---|---|
| Provider key | api_key_env on the LLM block, for an engine you run. subs llm set-key for a deployment. |
| Signing secret | signing_secret_env on the agent, for an engine you run. The deployment creates its own. |
| Connector token | auth.token_env on the connection, or subs mcp login. |
| Slack tokens | $SLACK_APP_TOKEN and $SLACK_BOT_TOKEN, or subs slack connect. |
subs apply strips api_key_env and signing_secret_env before it sends.