---
title: Settings
description: Typed values people tune on the Desk without editing the prompt. How they are declared, stored, snapshotted on each run, and used as filters.
sidebar:
  order: 4
---

## What it is

A setting is a typed value a person can change without touching the prompt: which markets to search, which title keywords count, which companies to ignore, how many Inbox messages to allow per run. Settings are declared in `routine.toml`, stored in the store, and edited on the Desk.

## Why it exists

People steer routines all the time. If steering means editing a prompt, only the builder can do it, every change risks breaking something else, and nobody can say later what a run was told. Settings make steering safe: each value has a type and a range, lint checks it, the Desk shows a proper control, and each run records exactly which values it used.

Settings are owned by people. A run never writes them. If a run thinks a value should change, it proposes the change in the Inbox; see [Learning](/concepts/learning).

## How it works

### Three layers

1. **Static**: the defaults in `routine.toml`, versioned with the routine.
2. **Per routine**: the values in `settings/<producerId>` under `values`, edited on the Desk.
3. **Per run**: one-off overrides when a run is triggered by a request.

The prompt reads layer 2, falls back to layer 1 when the document is missing (a missing document is treated as a first run), and the write step snapshots the values used onto the run document as `settingsUsed`. A later edit never changes what an earlier run is shown to have used.

### Seeding

`seed` writes the defaults to `settings/<producerId>` and `settings/workspace` only when they are absent. Reseeding never overwrites a saved value.

### Filters

A list setting with `role = "include"` or `role = "exclude"` filters records, through a `settingFilters` entry in the definition that names the field it checks.

- **Include** keeps only records whose field matches an entry. An empty include list keeps everything.
- **Exclude** drops records whose field matches an entry.
- Text fields match case-insensitive substrings: `commerce` matches `Ecommerce Developer`. Other field types match whole values, case-insensitive for strings.

A filtered record is not deleted. It is written with `status: "dropped"` and a reason: `not in <setting>` for an include miss, `excluded by <setting>` for an exclude hit. A record a person chose to keep is never dropped by a filter.

The substring rule came from a pilot run (2026-10-29-0438) in which whole-word matching dropped two Ecommerce roles.

### Changing a setting

On the Desk Settings page a person edits a value and saves. The page writes `settings/<producerId>` and shows "Saved. Applies from the next run." Page writes are last-writer-wins, and the page says so.

## Worked example

Hiring Signals declares five settings:

```toml
[[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"
```

| Setting | Type | Default | Filters |
|---|---|---|---|
| `markets` | list, include | AU, US, CA, NZ, PH | role `market` |
| `title_keywords` | list, include | Shopify, headless, commerce, AI automation | role `title` |
| `excluded_companies` | list, exclude | none (example `northwind.example`) | company `domain`, role `company_domain` |
| `lookback_days` | number, 1 to 30 | 7 | none |
| `max_inbox_per_run` | number, 0 to 20 | 5 | caps the Inbox |

In the [Quickstart](/introduction/quickstart) run, "Graduate Data Analyst" contains none of the title keywords, so it was written as dropped with the reason `not in title_keywords`, and the run recorded `droppedByReason: {"not in title_keywords": 1}`.

## Reference

### Setting keys

| Key | Applies to | Notes |
|---|---|---|
| `name` | all | Required. Name by effect, not mechanism |
| `type` | all | Required. `toggle`, `number`, `choice` or `list` |
| `default` | all | Required. Number defaults must sit inside `min` and `max`; choice defaults must be in `choices` |
| `label` | all | Text the Desk shows next to the control |
| `example` | all | A sample value shown as a hint |
| `cost_note` | all | What turning it up costs |
| `min`, `max` | number | Range; lint enforces |
| `choices` | choice | The allowed values |
| `role` | list | `include` or `exclude`; makes the list a filter |

### Workspace settings

`settings/workspace` holds settings for the whole Desk. Seed writes:

```json
{"values": {"notifications": "inbox", "quietHours": {"from": "", "to": ""}, "mergePolicy": {"company": "merge", "role": "merge"}},
 "updatedAt": "<seed time>", "updatedBy": "seed"}
```

Setting `values.learning: false` in it turns learning off for the workspace; see [Learning](/concepts/learning#the-off-switch).

### Never learnable

A setting whose name, with case and separators removed, contains `push`, `gate`, `send`, `alert`, `permission`, `budget`, `maxruns` or `capoverride` can never be changed by a proposal of any shape.

## Common pitfalls

- **Rules in the prompt instead of a setting.** If a person will ever change it, it is a setting, and if it filters records, it is a filter. The model does not reliably apply prose rules on every record.
- **An include list emptied by accident.** An empty include list keeps everything. The Desk refuses to accept a proposal that would empty one.
- **Expecting a change mid-run.** A saved value applies from the next run.
- **Two people editing at once.** Page writes are last-writer-wins. The second save replaces the first.
- **Naming a setting `alert_threshold` and expecting learning to tune it.** The name contains `alert`, so it is never learnable. Rename it if learning should touch it.

## Related

- [The definition](/concepts/definition)
- [Learning](/concepts/learning)
- [The Desk: Settings](/desk/settings)
- [routine.toml reference](/reference/routine-toml)
