# Routines

# Routines

Consumer-facing guide to `/routines` — scheduled Chats. A **Routine** is a cron that fires a [Chat](chat.md) on a cadence: each occurrence starts one **Master** agent turn in a **fresh Chat** named `"{Routine name} — {date}"`. Use Routines for standing work — refreshing a living document every Monday, a weekly competitive sweep, a daily pipeline pulse — anything you would otherwise re-ask by hand.

## The model

```
cron occurrence
   │
   ▼
new Session "Weekly pipeline refresh — 2026-07-13"
   │
   ▼
one Master turn, headless:
   - prompt = the routine's prompt (plus the doc's prior version, when declared)
   - config = the routine's persisted config (write_outputs, actions_allowed, …)
   - on_question is ALWAYS "none": ask_a_human returns no_human_in_loop immediately;
     a routine can never park waiting on a person
```

Every fire is a normal Chat run: watch it over the same SSE stream / read handles, find it in the Chats list, and read any document it committed in Living Knowledge. A routine that maintains a document commits through the verified write gate (see [Outputs](#outputs-the-document-commit-gate) below) — promotion to the current version stays human-gated unless every verification check passes.

## Create a routine

```bash
curl -X POST https://api.amdahl.com/api/platform/v1/routines \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Weekly pipeline refresh",
    "prompt": "Refresh the pipeline health report from current warehouse data.",
    "cron": "0 9 * * 1",
    "timezone": "America/New_York",
    "config": {
      "write_outputs": true,
      "agents": ["pipeline-analyst", "win-loss-analyst"]
    }
  }'
```

### Body fields

| Field      | Required | What it does                                                           |
| ---------- | -------- | ---------------------------------------------------------------------- |
| `name`     | yes      | Human label. Each fired Session is named `"{name} — {date}"`.          |
| `prompt`   | yes      | The Chat input every fire starts the Master turn with.                 |
| `cron`     | yes      | Cron expression, validated before the routine is stored.               |
| `timezone` | no       | IANA timezone the cron evaluates in. Default UTC.                      |
| `agent`    | no       | LEGACY single-agent pin. Prefer `config.agents`; passing both rejects. |
| `config`   | no       | Per-fire Chat config (below).                                          |
| `enabled`  | no       | Default `true`.                                                        |

### Config

The persisted subset of the Chat config, applied on every fire. `on_question` is not configurable — routines are headless, so a human ask always fails fast with `no_human_in_loop`.

| Key                  | Default    | Notes                                                                                                                                                                                                                                                                          |
| -------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `agents`             | —          | Who the routine employs: `"all"` (plain Master, unrestricted delegation), one ref (the Master turn runs AS that agent), or several refs (the Master may delegate ONLY to that roster; other agents and inline prompts are refused). Unset behaves like the legacy `agent` pin. |
| `write_outputs`      | **`true`** | Routines usually exist to refresh a document; the write gate still verifies.                                                                                                                                                                                                   |
| `write_memory`       | `false`    | Long-term memory commits.                                                                                                                                                                                                                                                      |
| `actions_allowed`    | `[]`       | Outbound actions the fired run may invoke.                                                                                                                                                                                                                                     |
| `external_search`    | `false`    | Market fan-outs.                                                                                                                                                                                                                                                               |
| `include_divergence` | `false`    | Divergence-map fusion on external reads.                                                                                                                                                                                                                                       |
| `evidence`           | `true`     | Citation/query refs on the answer.                                                                                                                                                                                                                                             |
| `depth`              | `standard` | `quick` / `standard` / `deep`.                                                                                                                                                                                                                                                 |

> The earlier `output_slug` / `full_regen` doc-target knobs are no longer writable — the agent picks its own document slug via `outputs.write_doc`. Existing routines that carry them keep their behavior (prior-version injection, wrote-no-doc detection) until their config is next replaced.

## Manage routines

| Verb                         | What it does                                                        |
| ---------------------------- | ------------------------------------------------------------------- |
| `GET /routines`              | List (filter `?enabled=`, paginate `?limit=&offset=`).              |
| `GET /routines/:id`          | One routine, including the last fire's Session + run ids.           |
| `PATCH /routines/:id`        | Patch name / prompt / cron / timezone / agent / config / enabled.   |
| `DELETE /routines/:id`       | Remove it. Sessions and documents from earlier fires are untouched. |
| `POST /routines/:id/run-now` | Fire immediately, off-cadence. Returns the Session + run handles.   |

`run-now` returns the same `{ chat_id, run_id, stream_url, read_url, resume_url }` envelope a Chat START does, so you can watch the fire live.

## Outputs: the document commit gate

A routine that refreshes a **living document** commits it through `outputs.write_doc` — the Master's only document-write path — never a raw knowledge-base upload. The gate enforces, in order:

1. **Run policy** — the run must have been started with `write_outputs: true` (the routine's config controls this per fire).
2. **Degraded-read halt** — if a theme read (`data.cluster_search`) FAILED during the run, the commit is blocked entirely. A failed read is not an absence of themes; shipping would silently overwrite the last good version.
3. **Verified promotion** — every stated digit is checked against the cells returned by the document's **bound query pack** (`outputs.run_bound_queries`, executed earlier in the same run). Auto-promotion to the current version is refused when no pack ran, when any digit fails verification, or when the run's pinned agent is tenant-authored — the new version then lands **proposed** and a human promotes it in Living Knowledge, which is the normal, safe outcome.

Documents keep their identity across every writer (routine fires, workflows, manual uploads): the slug resolves deterministically to the same per-workspace version family.

## Reliability semantics

- **Fresh Session per fire.** A routine never appends into an old Session; history is one Session per occurrence.
- **Missed occurrences self-heal.** A fire skipped across a deploy/restart is caught up once by the scheduler's reconcile pass (never once per skipped period).
- **No double-fires.** A cron tick and a catch-up for the same occurrence collapse to one run.
- **A failed fire is retried by the cadence.** If starting the Master turn fails, nothing is recorded and the reconcile catch-up re-fires it; there is no hot retry loop.
- **Roster fires re-validate.** Every agent in `config.agents` re-resolves at fire time; a routine pointing at a deleted agent skips the fire loudly instead of running with a partial roster.
- **Wrote-no-doc detection (legacy doc targets).** When a routine still declares `output_slug` and a fire completes without persisting a version, the next fire logs the gap loudly — and is itself the bounded redelivery.

## Canary: validating a routine against a workflow schedule

Before re-pointing a scheduled living document from a Workflow (blueprint schedule) onto a Routine, run both paths in parallel on one internal workspace and compare (this is the program's A9 gate; see the runbook §17):

1. Pick one `(workspace, doc_slug)` — e.g. `pipeline-health-report` on an internal tenant.
2. Leave the existing blueprint schedule enabled. Create a Routine with the same cadence and a prompt equivalent to the workflow's recipe intent that names the same doc slug (the Master commits it via `outputs.write_doc` with that slug).
3. Dry-run first: `POST /routines/:id/run-now` and watch the run via `stream_url`. Confirm the fire opened a NEW Session, ran `outputs.run_bound_queries` before writing, and landed a **proposed** version (never auto-promoted without a verified pack).
4. Compare the routine-written proposed version against the blueprint path's version in the doc's Versions tab: section headers present, key metrics match the pack cells, no "empty doc" signals.
5. Deliberately break a read (or pick a window with a degraded `cluster_search`) and confirm the routine run REFUSES the doc write (`degraded_read`) instead of committing a theme-less version.
6. Run 1–2 real cadences with both paths live, then write a short canary report before any cutover.

## Over MCP

Routines ride the Amdahl MCP server's `agents` tool (one tool covers the Chat lifecycle, the agent library, and its routines): `list_routines`, `get_routine`, `create_routine` (`name` + `prompt` + `cron`, optional `timezone` / `agent` / `config` / `enabled`), `update_routine`, `delete_routine`, and `run_routine_now` — the last returns the same Chat/run handles a `start_chat` does, so an MCP agent can fire a routine and watch it via the same tool's `chat_status` action. Routine reads are also MCP resources: `routine://list` and `routine://<id>`. Scopes: `routines:read` / `routines:write` (both on the customer-agent key bundle; existing keys were backfilled).

## Scopes

| Scope            | Grants                                                        | Default role |
| ---------------- | ------------------------------------------------------------- | ------------ |
| `routines:read`  | `GET /routines`, `GET /routines/:id`                          | viewer       |
| `routines:write` | create / update / delete / run-now                            | editor       |
| `outputs:write`  | `outputs.write_doc`, `outputs.run_bound_queries` (agent gate) | editor       |
