Config

Updated Sep 2, 2026View as Markdown

subs.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

subs.toml
name = "support-bot"
db = "subs.db"
log = "info"

[llm.claude]
type = "anthropic"

[agent.support]
llm = "claude"
model = "claude-sonnet-4-5"
system = "You are a support agent."
mcp = ["mcp.sentry"]
subagents = ["agent.researcher"]

[agent.researcher]
description = "Finds and reads sources."
llm = "claude"
model = "claude-haiku-4-5"

[mcp.sentry]
url = "https://mcp.sentry.dev/mcp"

[agent.support.slack]
name = "Support"

[serve]
host = "127.0.0.1"
port = 8080

[remote]
url = "https://api.substructure.ai"
org = "org_01hx…"
project = "proj_01hx…"

What a file describes

A file has two roles. It can have one or both.

RoleKeysCommands
An engine you rundb, log, [serve]subs run, subs serve
A deployment you administer[remote]subs apply, subs keys, subs sessions

The project itself stays the same for both roles: name, [llm.<id>], [agent.<id>], [mcp.<id>], and [plugin.<id>].

A second environment is a second file. subs apply -c subs.staging.toml deploys a separate project.

Top level

KeyTypeDefaultMeaning
namestringnoneThe project's name. subs apply creates the project from it and renames it when it changes.
max_subagent_depthinteger5How deep subagents may nest, for every agent. An agent's own value wins. See Subagents.
dbpath~/.config/subs/subs.dbThe SQLite file holding events, sessions, and connector credentials. A relative path resolves against the file.
logstringerror for run, info for serveA RUST_LOG filter. $RUST_LOG wins over it.

Unset, db is ~/.config/subs/subs.db, beside credentials.toml. Every command on the machine reads that one, whichever directory it runs in and whether or not a file is there. Set db to give a project its own. Two files that both set it 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"
KeyTypeDefaultMeaning
typeanthropic, openai, openrouter, workerrequiredWho makes the call.
api_key_envstringthe vendor's own variableThe variable holding the key. For an engine you run.
base_urlurlthe vendor's ownWhere to send the call.
formatopenai, anthropicthe engine's own shapeThe wire shape of the llm.execute a worker answers. type = "worker" only.
cache_ttl5m, 1h (anthropic, openrouter); in_memory, 24h (openai)the vendor's ownHow long the vendor holds a cached prompt prefix.

There is no default block and no fallback. An agent names a block, or its calls fail. See LLMs.

A worker block takes no api_key_env, base_url, or cache_ttl. The call never leaves your worker.

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 = ["mcp.sentry"]
subagents = ["agent.researcher"]
tools = [{ name = "confirm", description = "Ask a person", handler = "client" }]
worker = "https://bot.example.com/agent"
signing_secret_env = "SUPPORT_SIGNING_SECRET"

[agent.support.retry]
tool = { max_attempts = 3 }
KeyTypeDefaultMeaning
llmstringrequired with configThe [llm.<id>] block.
modelstringrequired with configThe model.
systemstringnoneThe system prompt.
effortstringthe provider's ownHow hard the model thinks: xhigh, high, medium, low, minimal, or none.
descriptionstringnoneWhat this agent does, shown to a parent that calls it.
mcplistnoneConnections this agent draws tools from. See mcp and plugins entries.
pluginslistnonePlugins this agent uses. See mcp and plugins entries.
defer_toolsbool or tableabsentKeeps every tool of this agent out of the request, whatever its source. See Defer every tool.
mcp_announceauto, neverautoWhether the engine tells the model that a connection is available. See Tell the model a connection exists.
mcp_auth_failureinterrupt, degradeinterruptThe default for every connection this agent reaches, including each plugin's. A connection overrides it with its own auth_failure.
mcp_tool_sync_failurewarn, silentwarnThe default for every connection this agent reaches, including each plugin's. A connection overrides it with its own tool_sync_failure.
subagentslistnoneAgents this one can call. Each entry is an agent.<id> path, or a table with id, defer, and prefix. See Subagents.
subagent_toolstable{ strategy = "per_agent" }What shape the subagents take as tools. per_agent offers one tool per agent; single offers one subagent tool for all of them. See Subagents.
max_subagent_depthinteger5How deep this agent's subagents may nest. A session that deep may not delegate; 0 never delegates. See Subagents.
toolslistnoneBrowser tools. Each needs handler = "client".
attachmentstablenoneHow files reach the model. See [agent.<id>.attachments].
workerurlnoneWhere decisions go. Leave it off and the engine decides.
signing_secret_envstringnoneThe variable holding the signing secret. For an engine you run.
retrytableengine defaultsTimeouts and attempts, per kind. See Retries.

An agent that sets no config needs a worker. An agent that sets any config needs llm and model. See Agents.

The tools your worker runs are worker code. They do not go in the file.

mcp and plugins entries

Each entry is an ID on its own, or a table. mcp takes a connection path such as mcp.sentry or plugin.pdf.mcp.renderer. plugins takes a plugin ID.

[agent.support]
mcp = [
  "mcp.sentry",
  { id = "mcp.linear", tools = { read_only = true }, approve = "destructive" },
]
KeyValuesDefaultMeaning
idpath or plugin IDrequiredWhich connection or plugin.
toolstableevery toolThe filter, and defer. See Filter the tools.
approvenever, destructive, alwaysneverWhich calls stop and ask a person. See Ask a person before a call runs.
auth_failureinterrupt, degradeinterruptWhat happens when the credential stops working. See When a credential stops working.
tool_sync_failurewarn, silentwarnWhether the model is told that a connection could not be reached. See Connection failures.

On a plugins entry these settings apply to each of the plugin's MCP servers.

Defer every tool

defer_tools = true takes the defaults. A table sets them. The presence of the key is the switch, so an agent cannot carry a setting that does nothing. A tool or a connection overrides it with its own defer.

KeyValuesDefaultMeaning
strategysearchsearchWhich tools find the deferred ones. search is the only value today.
max_matchesnumber, at least 15How many matches one search answers with.

See Deferred tools.

[agent.<id>.attachments]

How each file the agent receives reaches the model. Without this section every file rides in the prompt as media, and the agent offers no attachment tool.

[agent.analyst.attachments]
tools = ["read", "view"]
max_inline = "20mb"

[agent.analyst.attachments.rules]
"image/*"         = "inline"
"application/pdf" = "inline"
"*/*"             = "attachment"
KeyTypeDefaultMeaning
toolslistnoneThe attachment tools this agent offers: read, view, or both.
max_inlinesizeno limitA file over this size becomes an attachment, whatever its rule says. "20mb", or a number of bytes.
rulestablenoneMime pattern to inline or attachment.

A key in rules is a mime pattern: an exact type (application/pdf), a type wildcard (image/*), or */*. The most specific match wins. A file no pattern matches rides as media, the way it does without this section.

inline keeps the file in the prompt and adds one line naming it. attachment keeps the line alone. The line reads [attachment sales.csv text/csv 2.1 MB]. The id is the file name, with a counter when the name is already taken on this conversation. The bytes are stored in both cases, and the model reads them with attachment_read (text) or attachment_view (image, audio, and video).

The decision is made once, when the file arrives, and never changes.

The same table is attachments on the agent config, so a worker can set it at session.start like any other field. The rules are applied when a message is delivered to the worker, after session.start has settled the config, so they cover the first message of a session too.

[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"
prefix_tools = false
KeyTypeDefaultMeaning
urlurlrequiredThe server.
authstringask the serverOverride how it authenticates: "token", "oauth", or "none".
headerstringAuthorizationHeader a static token rides in. Only under auth = "token".
credentialstring"shared"Whose credential the connection dials with: "shared" for one, "user" for one per person.
scopeslistask the serverThe access to ask consent for. The server's own list is its maximum, not its recommendation.
client_id_envstringnoneVariable holding the OAuth client, for a server that issues none. Named, never written.
client_secret_envstringnoneThe secret half. Only alongside client_id_env.
prefix_toolsbooltrueShow the model <id>__<tool> instead of the connection's own names.

A token written in the file is a parse error. Fill a connection with subs auth <path>. See Connectors.

[plugin.<id>]

A plugin directory the CLI resolves and sends to the deployment. An agent names a plugin by its ID.

[plugin.pdf]
path = "./plugins/pdf-tools"

[plugin.pdf.mcp.renderer]
auth = "none"
KeyTypeDefaultMeaning
pathpathrequiredThe plugin directory.
mcp.<server>tablethe plugin's ownWhat this deployment says about one of the plugin's servers. See [plugin.<id>.mcp.<server>].

The CLI resolves the directory to data, at startup for a local engine and at subs apply for a deployment, so a session never reads plugin files. A plugin's servers join the connection registry as plugin.<id>.mcp.<server> and authorize like any connection. The model sees their tools under <plugin>_<server>__<tool>. See Plugins.

[plugin.<id>.mcp.<server>]

What this deployment says about one server the plugin declares, keyed by its name in the plugin's mcp.json. Every key overrides the plugin's. One left out keeps what the plugin shipped.

[plugin.pdf.mcp.renderer]
auth = "token"
url = "https://pdf.staging.example.com/mcp"
KeyTypeDefaultMeaning
urlurlthe plugin'sWhere this deployment reaches the server.
authstringask the server"oauth", "token", or "none". mcp.json has no field for it.
headerstringAuthorizationHeader carrying a static token. Only under auth = "token".
credentialstringsharedshared or user.
scopeslistnoneThe access to ask consent for.
client_id_envstringnoneVariable holding the OAuth client. Named, never written.
client_secret_envstringnoneThe secret half.
prefix_toolsbooltrueShow the model <id>__<tool>.

Authorize it by its path: subs auth plugin.pdf.mcp.renderer.

[agent.<id>.slack]

Gives one agent its own Slack app. The block being there is the whole declaration.

[agent.support.slack]
name = "Support"
description = "Answers customer questions"
answers = "both"
KeyTypeDefaultMeaning
namestringthe agent IDThe bot's name in Slack. Up to 35 characters.
descriptionstringnoneThe app's About text. Up to 140 characters.
answersboth, dm, channelsbothWhere the bot answers.

Every agent with this block is a separate Slack app. Which channels it answers in is decided in Slack, by who invites it.

Run subs auth agent.<id>.slack to set it up. See Slack.

[serve]

Defaults for subs serve.

KeyTypeDefaultMeaning
hoststring127.0.0.1The address to bind.
portnumber8080The port.
authbooltrueClient and worker authentication. Set false only for a server nothing off this machine can reach.
max_bodysize8mbThe largest request body the engine accepts. A file arrives base64, which is 4/3 its size, so this carries a file of about 6 MB. "8mb", or a number of bytes.
public_urlurlnoneThe HTTPS address a browser reaches this engine at. Setting it lets the engine mint MCP authorize links and host the callback. See Self-hosting.

max_body covers what reaches the engine: a client message and a worker's decision answer. It does not cover what the engine sends a worker. A worker that hosts the model receives the media in the prompt, so its own server must accept a body that size.

[remote]

The deployment this file administers. That can be the hosted cloud, one you host, or another person's subs serve.

KeyTypeDefaultMeaning
urlurlhttps://api.substructure.aiThe API to talk to.
orgIDnoneWritten by subs link or subs apply.
projectIDnoneWritten 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 > subs.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.

SecretHow the file refers to it
Provider keyapi_key_env on the LLM block, for an engine you run. subs auth for a deployment.
Signing secretsigning_secret_env on the agent, for an engine you run. The deployment creates its own.
Connector tokensubs auth <path>.
Slack tokens$SLACK_APP_TOKEN_<AGENT> and $SLACK_BOT_TOKEN_<AGENT>, or subs auth agent.<id>.slack.

subs apply strips api_key_env and signing_secret_env before it sends.

Next steps

  • Agents: what an agent section declares.
  • Plugins: what a [plugin.<id>] directory holds.
  • CLI: the commands that read this file.
  • Cloud: applying it to a deployment.
  • Protocol: the same types on the wire.