48 · agent.yaml and gateway.yaml, key by key
Language support
The selector reads turns written in English and in Brazilian Portuguese (pt-BR). The language is chosen, never guessed from the turn: the console's Settings page (Language the selector reads), language on a plan request, or the settings.language of your vocabulary file; with none of them, English. A Portuguese turn may be typed with its accents or without them. European Portuguese is not a grammar the selector has, and a turn in any other language is unread.
This chapter lists every key the two files take, with what each one means. Every table is generated from
the lists the stalwart-agents package checks a file against, so a key is here if and only if the package
reads it. A key the package does not know is an error at its line, never ignored: a misspelt word is never
a setting silently left at its default. Chapter 30 shows the files in use; this chapter
is the place to look a key up.
The top of agent.yaml
| Key |
What it says |
agents |
the agents, by name; the first is the one a conversation starts with |
tools |
where the tools come from: python: a module's public functions, mcp: a server's tools/list, openapi: each operation of a document, http: a web service's calls listed by hand, code: your own code in its own sealed space, or name: a tool's words |
rulebook |
the Colang policy every tool call is checked against and the graph is proved against |
facts |
what each fact the rulebook reads ($order_paid) is, read from a field of a tool's result: {from: get_order.status == paid, same: order_id} (chapter 45) |
runs |
atomic (the default): a failed run is undone, last write first; stop: a failed run is left as it is |
storage |
where runs and waiting confirmations are kept: sqlite:///runs.db, or postgres://... for several workers; in memory where not given |
stores |
the stores the tools read and write, such as a memory or a search index, and how each is shared |
budget |
limits on a run's loop, which the bug hunter's rule on unbounded loops reads |
modules |
the modules code a model writes may import, which the bug hunter's rules on model-written code read |
replies |
the reply check's settings (chapter 42) |
answers |
grounded answers from your documents (chapter 44) |
job |
the bot's job description: title, purpose, whom it serves (customers, staff or both), its manager and escalates_to (each a role), its team and its working hours |
tasks |
the bot's tasks, by name: each with its title, the tools it uses, its output (reply, document, draft or action), where it starts on the ladder (watch or suggest), the triggers that start it, and whether its changes run in_order or all_at_once (steps) |
triggers |
how work arrives by itself, by name: each of kind email, form, schedule, webhook, change or message; a schedule's timing is around, exact or always_on; a change's where takes conditions or a filter; a message arrives in teams, slack, whatsapp, instagram, messenger or sms, on an account |
ladder |
the bar a task must clear to move up the ladder (proposals 50, agreement 0.95 and drift_free_days 14 at the least: raised, never lowered) and the sample of Act-and-tell runs a person reviews |
drift |
when past work is replayed to watch for drift (replay: weekly or on_model_change) and the thresholds past which a signal is reported |
site |
the bot's own page: on, off, or its title and language |
job, tasks, triggers, ladder, drift and site describe the bot's work for the host that runs it
(below); the package checks them and passes them on.
An agent
Under agents:, each agent by its name:
| Key |
What it says |
agents.<name>.model |
the agent's model, provider:model, used with your own key from your environment; leave it out and no model is called |
agents.<name>.tools |
the tools this agent may call, by name |
agents.<name>.hands_off_to |
the agents this one may pass the conversation to |
agents.<name>.on_unread |
what a turn nothing reads does, once the person was asked to say it again: refuse, model, ask or answer; the console's setting where not given |
agents.<name>.privilege |
low, standard or high: what a hand-off from this agent is trusted with |
agents.<name>.remote |
how a hand-off to this agent travels when it runs elsewhere, and how that channel is authenticated |
agents.<name>.budget |
this agent's own limits on a run: max_steps, max_tool_calls and max_model_calls, in place of the file's |
agents.<name>.system |
what the model is told before the conversation |
agents.<name>.job |
this agent's own job description, in the words of the top-level job |
agents.<name>.defaults |
values the tools' arguments take wherever a turn gives none, by parameter name -- the account a tool acts on, say -- so the router never asks for them |
budget
At the top of the file, or on an agent in place of the file's:
| Key |
What it says |
budget.max_steps |
the most steps one run takes; on an agent, that agent's own, else the file's, else 60. The bug hunter's rule on unbounded loops (QG023) reads it |
budget.max_tool_calls |
the most tool calls one turn makes; the call past it is not made, and the turn stops with BudgetSpent |
budget.max_model_calls |
the most model calls one turn makes; the call past it is not made, and the turn stops with BudgetSpent |
remote
| Key |
What it says |
agents.<name>.remote.transport |
how a hand-off to this agent travels when it runs elsewhere, such as a2a |
agents.<name>.remote.authentication |
how the far side proves who it is, such as mtls; a remote agent with none is QG107 |
Each entry of tools: is one source, named by the key that says what kind it is, or a tool's own words
under name:. An entry of each kind takes only the keys in its table.
python:
| Key |
What it says |
tools[].python |
a module of your own, imported into the agent's process and trusted; every public function is a tool, its schema from its type hints |
mcp:
| Key |
What it says |
tools[].mcp |
an MCP server's address, or a file listing its tools; each tool it lists is a tool, with the hints the server gives |
tools[].url |
the address calls go to, where mcp: names a file that lists the tools |
tools[].version |
the server version the tools are pinned to |
tools[].digest |
the digest of the server's tool list the tools are pinned to |
tools[].headers |
headers sent on every request, each ${NAME} read from your environment when the request is made; how an agent sends its gateway token |
tools[].receipts |
true behind a gateway that admits each call on the agent's own check, which then travels with the call as a receipt |
tools[].tools |
only these of the server's tools; every one where not given |
openapi:
| Key |
What it says |
tools[].openapi |
an OpenAPI document; each operation is a tool, GET and HEAD reads, any other method a write unless its x-stalwart says otherwise |
tools[].headers |
headers sent on every request, each ${NAME} read from your environment when the request is made; how an agent sends its gateway token |
tools[].receipts |
true behind a gateway that admits each call on the agent's own check, which then travels with the call as a receipt |
An operation may say in its x-stalwart extension what its document cannot, with the tool words below and
one of its own:
| Key |
What it says |
x-stalwart.undo |
names the tool that undoes this write; a failed run calls it, checked like any call |
x-stalwart.irreversible |
the write cannot be undone; its confirmation says so, and a rollback leaves it in place |
x-stalwart.retry_safe |
calling it twice with the same arguments does what calling it once does, so a call whose outcome is unknown may be retried |
x-stalwart.reads_outside |
what it returns comes from outside, an e-mail or a web page, and is untrusted; what follows it carries that mark |
x-stalwart.sends_out |
it may send to a URL or another destination; a destination argument is refused on any tool that does not say this |
x-stalwart.after |
the tool runs only after each named tool, in the same session |
x-stalwart.not_after |
the tool never runs after a named tool, or after anything from outside where it names untrusted |
x-stalwart.at_most |
the tool runs at most this many times in a session; {count, per} and a list of those, one per period, are checked when the file is read, not yet applied |
x-stalwart.requires_privilege |
only an agent with at least this privilege may call it |
x-stalwart.argument_kinds |
what an argument is, such as code, sql, path or url, which the bug hunter's rules on model-written values read |
x-stalwart.confidential |
what the tool returns is confidential, and to whom |
x-stalwart.subject_argument |
the argument naming whose data the tool returns |
x-stalwart.idempotency |
{header: <name>}: the header a call's idempotency key is sent in |
x-stalwart.also_called |
words a person uses for what the tool acts on, read as the words of its name are |
http:
| Key |
What it says |
tools[].http |
the address of a REST API with no OpenAPI document, starting https:// |
tools[].headers |
headers sent on every request, each ${NAME} read from your environment when the request is made; how an agent sends its gateway token |
tools[].operations |
the API's calls, listed by hand, each a tool (the next table) |
tools[].receipts |
true behind a gateway that admits each call on the agent's own check, which then travels with the call as a receipt |
Each of its operations:, besides any tool word:
| Key |
What it says |
tools[].operations[].name |
the tool's name |
tools[].operations[].method |
GET, a read, or POST, PUT, PATCH or DELETE, each of which must say whether it writes |
tools[].operations[].path |
the path after the API's address |
tools[].operations[].schema |
the arguments' JSON Schema; beside it, any tool word |
code:
| Key |
What it says |
tools[].code |
your own code, to run isolated in its own sandbox, each function a tool; a folder beside agent.yaml, or a Git URL pinned to a commit. Checked and kept; running it in its sandbox is still to come |
tools[].language |
the code's language; python, for now |
tools[].requirements |
the file of pinned requirements (name==1.2.3), installed when the sandbox is built |
tools[].functions |
only these functions; every public one where not given |
tools[].state |
a folder inside the project kept between calls |
tools[].mode |
call, one run per tool call, or service, which the host keeps running |
tools[].sandbox |
the sandbox's bounds (the sandbox table) |
Its sandbox::
| Key |
What it says |
tools[].sandbox.level |
standard, the default, which may only tighten the standard bounds, or advanced, which lets the owner widen them, each widening accepted by a person on the host |
tools[].sandbox.network |
the hosts the code may reach; none where not given; any at the advanced level only |
tools[].sandbox.secrets |
environment variables passed into this sandbox only, by name, never by value; never one of Stalwart's own |
tools[].sandbox.limits |
the time, memory and processes a run may use (the limits table) |
tools[].sandbox.mounts |
host folders the code may see, at the advanced level only |
tools[].sandbox.allow |
the relaxations the host's allow list names, at the advanced level only |
The sandbox's limits::
| Key |
What it says |
tools[].sandbox.limits.seconds |
the seconds a run may take; 60 at most at the standard level, and 0, no limit, only with mode service |
tools[].sandbox.limits.memory_mb |
the memory a run may use, in megabytes; 1024 at most at the standard level |
tools[].sandbox.limits.cpus |
the processors a run may use; 1 at most at the standard level |
tools[].sandbox.limits.processes |
the processes a run may start; 32 at most at the standard level |
Each of its mounts::
| Key |
What it says |
tools[].sandbox.mounts[].path |
the host folder; never the host's root, its system folders, Stalwart's own or the project's, and never a relative path |
tools[].sandbox.mounts[].write |
true where the code may write there |
The floor. These are refused at every level, wherever they appear, because they would reach Stalwart
itself or the owner's other bots:
| Key |
What it says |
privileged |
a privileged container |
capabilities |
added Linux capabilities |
devices |
the host's devices |
docker_socket |
the Docker socket |
host_network |
the host's network |
gate |
switching off the gate |
audit |
switching off the audit |
stop_button |
switching off the stop button |
Under name:, or as @tool(...) in Python, or beside an http: operation:
| Key |
What it says |
writes |
the tool changes something, so its call is checked against your rules, and waits for a person where they say so or where nothing allows it |
undo |
names the tool that undoes this write; a failed run calls it, checked like any call |
irreversible |
the write cannot be undone; its confirmation says so, and a rollback leaves it in place |
retry_safe |
calling it twice with the same arguments does what calling it once does, so a call whose outcome is unknown may be retried |
reads_outside |
what it returns comes from outside, an e-mail or a web page, and is untrusted; what follows it carries that mark |
sends_out |
it may send to a URL or another destination; a destination argument is refused on any tool that does not say this |
after |
the tool runs only after each named tool, in the same session |
not_after |
the tool never runs after a named tool, or after anything from outside where it names untrusted |
at_most |
the tool runs at most this many times in a session; {count, per} and a list of those, one per period, are checked when the file is read, not yet applied |
confirm_with |
what a person is asked before the call runs, in your words; an irreversible tool that gives none is asked about as Run <tool>? Nothing undoes it once it has run. |
requires_privilege |
only an agent with at least this privilege may call it |
description |
what the tool does, which the selector reads turns against; the docstring's first line where not given |
checks |
conditions each call must meet, such as a quantity at most what is in stock; checked when the file is read, not yet applied to a call |
filter |
on a read, which records it hands on: each names the list, keep the conditions each record meets; checked when the file is read, not yet applied |
spend |
the most a field may add up to per period, tools with the same pool sharing one count; checked when the file is read, not yet applied |
currency |
the currency the tool's amounts are in, as USD or BRL; required with spend |
outcome |
on a write, the read that follows it to its end and what done and failed look like; checked when the file is read, not yet applied |
argument_kinds |
what an argument is, such as code, sql, path or url, which the bug hunter's rules on model-written values read |
sandboxed |
the code the tool runs is contained, which lowers the severity of a finding about it |
sanitised_by |
the check that cleans an argument before the tool uses it |
confidential |
what the tool returns is confidential, and to whom |
confidential_fields |
which fields of what it returns are confidential |
subject_argument |
the argument naming whose data the tool returns |
retrieves |
the store the tool reads from, and the argument its reads are scoped by |
persists |
the store the tool writes into |
source |
where the tool's code comes from, which the bug hunter's rule on unpinned sources reads |
server |
the MCP server that serves the tool, with its version or digest |
also_called |
words a person uses for what the tool acts on, read as the words of its name are |
Conditions and limits on a tool
A tool's checks, its filter, its at_most, its spend and its outcome share one grammar. Each
condition is about one thing and has exactly one operator:
| Key |
What it says |
checks[].field |
the value the condition is about: an argument, or a field of the result, by name or dotted path, as order.total; a path through a list, as items[].price, holds only where every item does |
checks[].total |
the sum over a list's items, as legs[].cost; it compares as a number |
checks[].count |
how many items a list holds, as legs[]; it compares as a number |
checks[].any |
a list of conditions, any one of which is enough; a plain list of conditions means all of them |
checks[].is |
the value equals this |
checks[].is_not |
the value differs from this |
checks[].one_of |
the value is one of a list, or of what an earlier call returned, as {from: get_slots.slots[].start} |
checks[].none_of |
the value is none of a list, or of what an earlier call returned |
checks[].at_least |
the number is this or more; the operand may be {from: <tool>.<field>}, what an earlier call in the run returned |
checks[].at_most |
the number is this or less, as a quantity at most {from: check_stock.in_stock} |
checks[].under |
the number is less than this |
checks[].over |
the number is more than this |
checks[].between |
the number, date or time is within two bounds, lowest first, as [10, 20] |
checks[].before |
the date or time is earlier than this, written as 2026-10-16, 17:30 or 2026-10-16T17:30 |
checks[].after |
the date or time is later than this |
checks[].contains |
the value contains this text |
checks[].because |
the reason in your own words, shown when the condition stops something |
checks[].unknown |
what a value nobody could read does: fail, the default, or pass, which only a filter and a change trigger's where may say, never a check that lets a call through |
filter:, on a read:
| Key |
What it says |
filter.each |
the list in the result whose records the read hands on, ending in [], as jobs[] |
filter.keep |
the conditions each record must meet to be handed on; inside them, {from_row: <field>} is another field of the same record |
at_most: is a number of calls per session, or one of these, or a list of them:
| Key |
What it says |
at_most.count |
the most calls in the period, a whole number from 1 |
at_most.per |
the period counted over: call, run, session, minute, hour, day, week or month; one limit per period |
Each of spend:, with the tool's currency:
| Key |
What it says |
spend[].field |
the amount's field, as amount, or legs[].cost for the items together |
spend[].at_most |
the most the amounts may add up to in the period, a number above 0, in the tool's currency |
spend[].per |
the period counted over, as day or month; the same periods as at_most |
spend[].pool |
a name; tools with the same pool share one counter |
outcome:, on a write:
| Key |
What it says |
outcome.follow |
the read tool that reports how the write ended; required, and it must be a declared tool that does not write |
outcome.same |
the field that links the write and the read, as order_id |
outcome.done_when |
the conditions on the read's result that say the write is done; required |
outcome.failed_when |
the conditions on the read's result that say the write failed |
outcome.give_up_after |
the minutes to wait, at least 1, before the outcome is unknown |
Stores and modules
Each of stores:, by the store's name:
| Key |
What it says |
stores.<name>.persistent |
true where what the store holds outlives a run, which the hunter's rules on planted text read (QG021) |
stores.<name>.multi_tenant |
true where several tenants share the store, so what one writes another may read |
Each of modules:, by the module's path:
| Key |
What it says |
modules.<path>.version |
the version a Colang action module is pinned to |
modules.<path>.digest |
the digest a Colang action module is pinned to |
Facts
Each of facts:, by the name the rulebook reads it under:
| Key |
What it says |
facts.<name>.from |
the tool result the fact is read from, as tool.field == value or tool.field; never a tool that reads outside |
facts.<name>.same |
the argument, list of arguments, or mapping of arguments that ties the fact to the call being checked, so one order's status never allows a refund of another |
Replies and answers
replies::
| Key |
What it says |
replies.rules |
the reply rules file, as the console's Reply rules page saves it |
replies.phrasings |
the outcome phrasings file |
replies.composer |
model: the model writes the reply and the check holds it; template: the reply is the phrasings, and no model writes it |
replies.language |
the default phrasings' language: en or pt |
answers::
| Key |
What it says |
answers.index |
the source index stalwart sources build writes |
answers.k |
at most this many passages; 3 where neither the file nor the console says |
answers.floor |
no passage scoring below this is used, from 0 to 1; 0.3 where neither says |
answers.composer |
extract, the documents' own sentences; or compose, your model writes from them |
The bot's work: job, tasks and triggers
These keys describe the bot to the host that runs it. The package checks each of them when it reads the
file; what each one does is the host's.
| Key |
What it says |
job |
the bot's job description: title, purpose, whom it serves (customers, staff or both), its manager and escalates_to (each a role), its team and its working hours |
tasks |
the bot's tasks, by name: each with its title, the tools it uses, its output (reply, document, draft or action), where it starts on the ladder (watch or suggest), the triggers that start it, and whether its changes run in_order or all_at_once (steps) |
triggers |
how work arrives by itself, by name: each of kind email, form, schedule, webhook, change or message; a schedule's timing is around, exact or always_on; a change's where takes conditions or a filter; a message arrives in teams, slack, whatsapp, instagram, messenger or sms, on an account |
ladder |
the bar a task must clear to move up the ladder (proposals 50, agreement 0.95 and drift_free_days 14 at the least: raised, never lowered) and the sample of Act-and-tell runs a person reviews |
drift |
when past work is replayed to watch for drift (replay: weekly or on_model_change) and the thresholds past which a signal is reported |
site |
the bot's own page: on, off, or its title and language |
job
| Key |
What it says |
job.title |
the job's name |
job.purpose |
what the bot is for, in a sentence |
job.serves |
whom it serves: customers, staff or both |
job.manager |
the role that manages it; the host names the person |
job.team |
who gives it work and who approves (the team table) |
job.hours |
its working hours (the hours table) |
job.escalates_to |
the role a stuck card goes to |
Its team::
| Key |
What it says |
job.team.gives_work |
the roles that give it work |
job.team.approves |
the roles that approve what it proposes |
Its hours::
| Key |
What it says |
job.hours.days |
the working days, as mon to sun |
job.hours.from |
the time work starts, as HH:MM |
job.hours.to |
the time work ends, as HH:MM |
job.hours.time_zone |
the time zone the hours are in, such as Europe/London |
tasks
Each task, by its name:
| Key |
What it says |
tasks.<name>.title |
the task's name in words; required |
tasks.<name>.tools |
the tools the task uses, each declared under tools |
tasks.<name>.agent |
the agent that does it; the first where not given |
tasks.<name>.serves |
whom the task serves: customers, staff or both |
tasks.<name>.output |
what it produces: reply, document, draft or action |
tasks.<name>.starts_at |
the ladder level it starts at: watch or suggest; no task starts higher |
tasks.<name>.triggers |
the triggers that start it, each declared under triggers |
tasks.<name>.run_window |
minutes a sleeping host may still start a due run, from 1 to 1440 |
tasks.<name>.form |
true where people may start the task by hand |
tasks.<name>.steps |
in_order, or all_at_once: every write lands together, or none does |
triggers
Each trigger, by its name, is one kind and the keys that kind takes:
| Key |
What it says |
triggers.<name>.kind |
how work arrives: email, form, schedule, webhook, change or message; it decides the keys beside it |
triggers.<name>.inbox |
the inbox's name |
triggers.<name>.urgent |
true where the work does not wait for working hours |
| Key |
What it says |
triggers.<name>.kind |
how work arrives: email, form, schedule, webhook, change or message; it decides the keys beside it |
triggers.<name>.audience |
who fills the form in: customers or staff |
triggers.<name>.urgent |
true where the work does not wait for working hours |
| Key |
What it says |
triggers.<name>.kind |
how work arrives: email, form, schedule, webhook, change or message; it decides the keys beside it |
triggers.<name>.at |
the time it is due, as HH:MM |
triggers.<name>.days |
the days it is due, as mon to sun |
triggers.<name>.timing |
around the time, exact, or always_on |
triggers.<name>.urgent |
true where the work does not wait for working hours |
| Key |
What it says |
triggers.<name>.kind |
how work arrives: email, form, schedule, webhook, change or message; it decides the keys beside it |
triggers.<name>.secret |
the signing secret's environment variable, as ${NAME}; never the secret itself |
triggers.<name>.urgent |
true where the work does not wait for working hours |
| Key |
What it says |
triggers.<name>.kind |
how work arrives: email, form, schedule, webhook, change or message; it decides the keys beside it |
triggers.<name>.tool |
the tool it polls; it names this or resource, not both |
triggers.<name>.resource |
the resource it subscribes to; it names this or tool, not both |
triggers.<name>.every |
minutes between polls, from 1 to 1440 |
triggers.<name>.where |
conditions on the polled result's fields, such as {field: open_count, at_least: 1} |
triggers.<name>.urgent |
true where the work does not wait for working hours |
| Key |
What it says |
triggers.<name>.kind |
how work arrives: email, form, schedule, webhook, change or message; it decides the keys beside it |
triggers.<name>.in |
the channel: teams, slack, whatsapp, instagram, messenger or sms |
triggers.<name>.account |
the page, number or workspace: a name, or ${NAME} for one the host holds |
triggers.<name>.urgent |
true where the work does not wait for working hours |
ladder, drift and site
| Key |
What it says |
ladder.bar |
what a task must show to move up a level: proposals, agreement and drift_free_days, raised, never lowered below 50, 0.95 and 14 |
ladder.sample |
the share of Act-and-tell runs a person reviews; 0.1 where not given |
| Key |
What it says |
drift.replay |
weekly, or on_model_change only |
drift.thresholds |
how far each signal may move before it is drift: tools, plan_shape, decisions, plan_lock_stops, refusals, untraced_claims |
drift.baseline_days |
the days of runs the signals are compared against, from 1 to 90 |
| Key |
What it says |
site.title |
the site's title |
site.language |
the site's language: en or pt |
site.colour |
the site's main colour |
site.logo |
the site's logo |
A template
A template is an agent.yaml a person starts from. It adds:
| Key |
What it says |
first_bot |
true where the template starts Make my first bot |
needs |
the catalogue entries the template uses, each with its sign-in (the next table) |
optional |
entries it offers and works without |
Each of its needs::
| Key |
What it says |
needs.<name>.entry |
the catalogue entry's id; required |
needs.<name>.sign_in |
how the entry signs in: oauth, service or none |
needs.<name>.why |
why the template needs it, in a sentence the owner reads |
needs.<name>.route |
how its data is reached: api, aggregator, owner_feed or owner_browser |
gateway.yaml
The gateway (chapter 37) reads these from the file it is started with, and refuses to start
on any other:
$ stalwart gateway --config gateway.yaml
| Key |
What it says |
catalogue |
the tenant's catalogue the gateway checks every call against, in the extracted-model format; required |
upstreams |
which MCP server holds each tool, by tool name; "*" for the rest |
agents |
one token per agent, each ${NAME} from the environment; every agent connects with its own, and at least one is required |
admin_token |
the token the rollback route takes; the route is off where it is not set |
audit |
the file every decision is appended to |
elicitation_timeout |
seconds a person has to answer a question the call asks; 600 where not given |
listen |
the address and port it serves on; 127.0.0.1:8800 where not given |
The limits
- A key's table says what the package reads. Some keys are checked and kept but not yet acted on --
checks, filter, spend and outcome on a tool, and a code: source's code, which is not yet run --
and their rows say so.
- The founding keys are checked here and acted on by the host that runs the bot.