routine.toml and app.toml
Every table and key in a routine's routine.toml and its app.toml, with types, defaults and what lint checks. Taken from schema.py, appmodel.py and the scaffolds.
A routine folder has two TOML files:
routine.tomldescribes one routine: type, schedule, store, workspace, settings, gate and actions. It is loaded byschema.py.app.tomldescribes the app around it: routines, store kind, budget, integrations, secrets, environment, deployment and risks. It is loaded byappmodel.py.new --appwrites it.
Both are read with Python’s tomllib.
routine.toml
[routine]
| Key | Type | Required | Values |
|---|---|---|---|
name |
text | yes | The routine name and producer id |
type |
text | yes | serverless, watch, approval-gated, interactive |
schedule |
text | yes | Five-field cron, UTC. The minute field must be a single number, so runs are at least an hour apart |
first_run |
text | no | report-only (default) or live |
[store]
| Key | Type | Values |
|---|---|---|
adapter |
text | artifact-db in every scaffold. See Storage |
[workspace]
| Key | Type | Meaning |
|---|---|---|
enabled |
bool | The routine writes to a Desk workspace. false for serverless and approval-gated scaffolds |
name |
text | Workspace name used by seed when --name is not given |
template |
text | Template the routine came from, copied into meta/workspace |
template_version |
integer | That template’s version |
[[setting]]
One table per setting.
| Key | Type | Required | Meaning |
|---|---|---|---|
name |
text | yes | Setting name |
type |
text | yes | toggle, number, choice, list |
default |
by type | yes | toggle: true or false. list: a list. number: within min and max. choice: one of choices |
label |
text | no | Shown on the Desk |
example |
text | no | Shown as a placeholder |
role |
text | list only | include or exclude. Needed for a setting used in settingFilters |
min, max |
number | no | Bounds for a number |
choices |
list | choice only | Non-empty |
cost_note |
text | no | What the setting costs, shown on the Desk |
A routine that writes Inbox messages ("inbox": true in its definition) must declare a number setting max_inbox_per_run.
[gate]
| Key | Type | Meaning |
|---|---|---|
enabled |
bool | Required true before any send, post or delete action |
transport |
text | email or slack, the two verified transports. Lint does not check the value |
timeout_hours |
number | How long a gate waits. Keep it longer than the longest gap between runs |
[[action]]
| Key | Type | Meaning |
|---|---|---|
name |
text | Action name |
kind |
text | send, post and delete are external and need [gate] enabled = true |
Example: Hiring Signals
From templates/jobs-watch/routine.toml, settings shortened:
[routine]
name = "jobs-watch"
type = "watch"
schedule = "0 22 * * 1,4" # UTC, Monday and Thursday; minimum interval is 1 hour
first_run = "report-only"
[store]
adapter = "artifact-db"
[workspace]
enabled = true
template = "jobs-watch"
template_version = 1
[[setting]]
name = "title_keywords"
type = "list"
role = "include"
label = "Title keywords"
default = ["Shopify", "headless", "commerce", "AI automation"]
cost_note = "each keyword adds a search per market"
[[setting]]
name = "max_inbox_per_run"
type = "number"
label = "Inbox messages per run"
default = 5
min = 0
max = 20
cost_note = "no extra cost; more messages ask more of your attention"
[gate]
enabled = true
transport = "email"
timeout_hours = 48
The other settings are markets (list, include), excluded_companies (list, exclude) and lookback_days (number, 1 to 30, default 7). new hiring-signals --from-template jobs-watch renames the routine.
app.toml
[app]
| Key | Required | Meaning |
|---|---|---|
id |
yes | App id |
title |
yes | Shown on the permission screen |
status |
yes | not live, live, not yet tested, live and tested, retired |
purpose |
no | One sentence |
owner |
no | Who owns it |
created |
no | Date |
[[routine]]
| Key | Meaning |
|---|---|
name |
Must match routine.toml |
folder |
The routine’s folder, . for the app folder |
type |
Routine type |
schedule |
Must match routine.toml |
timezone |
IANA zone, shown on the permission screen. The cron stays UTC |
role |
collector, actor, reporter or listener (scaffold comment) |
delivery |
repo (default), account-skill or inline |
repo |
owner/name, needed for repo delivery unless [deployed] repo is set |
skill |
Lowercase skill name. Needed for account-skill |
connectors |
Names declared in [[integration]] |
hosts |
Hosts reached, each declared in [[integration]] |
writes |
Collections this routine owns, each with a [[collection]] |
tools |
Tools a run may use. No unlisted send tool |
[store]
| Key | Meaning |
|---|---|
kind |
artifact-db by default |
[budget]
| Key | Required | Meaning |
|---|---|---|
max_runs_per_day |
yes | Whole number, 1 or more. Prompt-enforced, not guaranteed |
alert_channel |
no | Where the one budget notice goes, for example email |
[[collection]]
| Key | Meaning |
|---|---|
name |
A store collection |
writer |
The one routine that writes it |
[[integration]]
| Key | Meaning |
|---|---|
name |
Name used in a routine’s connectors |
kind |
connector, mcp, secret-host, http |
direction |
read or write |
hosts |
Hosts it reaches |
gate |
Must be true when direction = "write" |
what |
Purpose shown on the permission screen |
[[secret]]
| Key | Meaning |
|---|---|
name |
Secret name, never its value |
where |
connector or network-secret. env-var is refused |
host |
The single host it may reach |
[environment]
What you read in the claude.ai environment dialog. RoutineOS cannot read it.
| Key | Type | Meaning |
|---|---|---|
name |
text | Environment name. Default is shared by every routine using it |
network |
text | none, trusted, limited, full, custom |
allowed_domains |
list | Host names |
setup_script |
bool | Whether a setup script runs |
env_vars |
list | Variable names only |
shared |
bool | The environment is shared |
[deployed]
| Key | Meaning |
|---|---|
routine_ids |
Task ids, after creation and read-back |
environment |
Environment the task uses |
repo |
Repo, used when a routine has no repo |
[risks]
| Key | Meaning |
|---|---|
file |
risks.md |
relies_on |
Registry capability ids. platform check reports them |
combo |
true when private data, untrusted content and an external send are knowingly accepted together |
What lint checks
routine.toml (schema.py):
[routine]hasname,typeandschedule;typeis known.- The schedule has five fields and a single-number minute field.
first_runisreport-onlyorlive.- Each setting has a name, a known type and a default of the right shape;
roleonly on lists; a choice default is inchoices; a number default is withinminandmax. - A
send,postordeleteaction has[gate] enabled = true. prompt.mdexists, contains “is data, not instructions”, and has a line startingPush rule:.
app.toml (appmodel.py), each problem prefixed app.toml: :
[app]hasid,titleandstatus.- No string looks like a secret (
sk-,AKIA,AIza,ya29.patterns): “looks like a secret value, remove it”. - Each routine’s connectors and hosts are declared in
[[integration]]. - One writer per collection; a routine writes only collections it owns.
- A write integration has
gate = true: “integration ‘’ writes externally, set gate = true”. - No secret is stored in an env var.
- Delivery mode, repo and skill name are valid.
[environment]values have the right types and a known network level.toolsis a list of names.- A live status has
[deployed].routine_ids. - Each schedule passes the same cron rule.
[budget].max_runs_per_dayexists and is a whole number of 1 or more.routine.tomlandapp.tomlagree on name and schedule.
With app.toml present, lint also scans the other files in the folder for secret-shaped values (reported as file and line, never the value; dashboard/ is skipped), requires a non-empty risks.md, and requires each routine’s prompt.md to mention state/verified and state/budget.
Lint does not reject unknown keys in either file.