Config

Updated Oct 29, 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>], [plugin.<id>], and [skill.<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.
send_timeout_secsinteger30How long the engine waits to hand off work or a batch of events. After that it tries again later.

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.

[system1.<id>]

Where a system1.call runs. A worker names a block by its ID.

[system1.cf]
type = "workers-ai"
model = "clef-flash"
account_id = "0123abcd…"
price = { input = "0.038" }
KeyTypeDefaultMeaning
typetypesafe, workers-ai, httprequiredWho answers the call.
modelstringnoneThe model a call uses when it names none.
api_key_envstringTYPESAFE_API_KEY, CLOUDFLARE_API_TOKEN, none for httpThe variable holding the key. For an engine you run.
base_urlurlthe vendor's ownWhere to send the call. typesafe and workers-ai only.
urlurlrequired for httpThe endpoint. http only.
account_idstring$CLOUDFLARE_ACCOUNT_IDThe Cloudflare account. workers-ai only.
questionslist of noul, choice, score, boxwhat the engine knows of the modelThe question types the model answers. The engine refuses others before the call.
price{ input = "<decimal>" }noneThe price per million input tokens. It sets the call's cost.

See System One models.

[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.
skillslistnoneThe IDs of the [skill.<id>] sections this agent uses. See Skills.
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.
toolslistnoneTools the worker or the browser runs: name, description, input, output, handler and defer. Leave out handler for the worker, or set handler = "client". input and output are a JSON Schema, or the path of a file that holds one.
attachmentstablenoneHow files reach the model. See [agent.<id>.attachments].
contexttablenoneHow the agent makes room when the model reads too much. See [agent.<id>.context].
codemodejavascriptnoneOffers run_code, which runs a script in this language that calls the agent's tools. See Code mode.
workerurlnoneWhere decisions go. Leave it off and the engine decides.
eventsstringnoneThe [events.<id>] block that gets this agent's events. See Events URL.
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 can go in the file, but they do not have to: the worker can return them in the config. An agent with a tool, a skill, or a plugin that the worker answers needs a worker.

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.
skillslist, searchlistlist names every skill in the system prompt. search names none there, and tool_search finds them. See Skills.

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.

[agent.<id>.context]

How the agent makes room in what the model reads. Before a model call, the engine tries the strategies in order. See Make room.

[agent.support.context]
window = 200000

[[agent.support.context.strategy]]
use = "reset"
idle_secs = 3600

[[agent.support.context.strategy]]
use = "summarize"
keep_tokens = 20000
KeyTypeDefaultMeaning
windowintegernoneThe model's context window in tokens.
strategylistnoneThe strategies, in order.

Each strategy:

KeyTypeDefaultMeaning
usereset, summarize, workerrequiredWhat the engine proposes. reset keeps the turn's own messages. summarize summarizes the older messages. worker proposes the held call as it was.
idle_secsintegernoneApplies at a turn's first model call, after this many seconds with no turn.
over_tokensintegerthe window less 16000, without idle_secsApplies when the model would read more than this.
keep_tokensinteger20000summarize: about how many recent tokens stay as they are, from a user message.
llmstringthe agent'ssummarize: the [llm.<id>] block that writes the summary.
modelstringthe agent'ssummarize: the model that writes the summary.
promptstringthe engine'ssummarize: the instructions for the summary. The engine puts the conversation before them.

The same table is context on the agent config.

[events.<id>]

A URL that gets the events and the live text of the sessions of each agent that names it. See Events URL.

[events.app]
url = "https://app.example.com/events"
KeyTypeDefaultMeaning
urlurlrequiredWhere the engine POSTs events and live text.
timeout_secsinteger10How long one POST may take before the engine sends it again later. At most 15.

[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, or a plugin whose skills the worker answers. An agent names a plugin by its ID. An ID that starts with _ is the engine's.

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

[plugin.pdf.mcp.renderer]
auth = "none"
KeyTypeDefaultMeaning
pathpathrequired, unless source = "worker"The plugin directory.
mcp.<server>tablethe plugin's ownWhat this deployment says about one of the plugin's servers. See [plugin.<id>.mcp.<server>].
sourcebundle, workerbundleWhere the skills come from. worker takes no path and no servers.
descriptionstringnoneWhat the plugin is for. Only with source = "worker".
skillslistnoneEach skill's name, description and tools. Only with source = "worker". See A plugin the worker answers.

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.

[skill.<id>]

A skill of the project's own: a directory with a SKILL.md, or a skill that the worker answers. An agent names it in skills. See Skills.

[skill.automations]
path = "./skills/automations"

[skill.visits]
source = "worker"
description = "Who visited the clinic, and when"
tools = ["find_bookings"]
KeyTypeDefaultMeaning
pathpathrequired, unless source = "worker"The skill directory. Its name, and the name in its front matter, must be the section's ID.
sourcebundle, workerbundleWhere the skill comes from. worker takes no path.
descriptionstringthe front matter'sWhat the model reads before it loads the skill. Required with source = "worker".
toolslistthe front matter'sThe tools that the model cannot see or call until it loads the skill.

[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.
Events signing secretsubs secret events.<id>, for an engine you run.
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.
  • Skills: what a [skill.<id>] section declares.
  • CLI: the commands that read this file.
  • Cloud: applying it to a deployment.
  • Protocol: the same types on the wire.