---
title: routine.toml and app.toml
description: 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.
sidebar:
  order: 2
---

A routine folder has two TOML files:

- **`routine.toml`** describes one routine: type, schedule, store, workspace, settings, gate and actions. It is loaded by `schema.py`.
- **`app.toml`** describes the app around it: routines, store kind, budget, integrations, secrets, environment, deployment and risks. It is loaded by `appmodel.py`. `new --app` writes 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](/infrastructure/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:

```toml
[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]` has `name`, `type` and `schedule`; `type` is known.
- The schedule has five fields and a single-number minute field.
- `first_run` is `report-only` or `live`.
- Each setting has a name, a known type and a default of the right shape; `role` only on lists; a choice default is in `choices`; a number default is within `min` and `max`.
- A `send`, `post` or `delete` action has `[gate] enabled = true`.
- `prompt.md` exists, contains "is data, not instructions", and has a line starting `Push rule:`.

`app.toml` (`appmodel.py`), each problem prefixed `app.toml: `:

- `[app]` has `id`, `title` and `status`.
- 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 '<name>' 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.
- `tools` is a list of names.
- A live status has `[deployed].routine_ids`.
- Each schedule passes the same cron rule.
- `[budget].max_runs_per_day` exists and is a whole number of 1 or more.
- `routine.toml` and `app.toml` agree 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.

## Related

- [CLI reference](/reference/cli)
- [Definition schema](/reference/definition-schema)
- [Settings](/concepts/settings)
- [Build your first routine](/guides/first-routine)
