> ## Documentation Index
> Fetch the complete documentation index at: https://docs.timeless.day/llms.txt
> Use this file to discover all available pages before exploring further.

# Playbooks

> How the CLI turns meetings into artifacts — a named prompt plus an inline trigger, run by your agent harness.

A **playbook** is the instruction the CLI hands to your agent. Each playbook is keyed
by a **name** and carries its own trigger in an `on:` block, and
[`timeless playbooks run`](/cli/commands#playbooks-run) renders its prompt through your
configured [harness](/cli/config) to produce an artifact.

<Note>
  **Your machine has to be on.** Playbooks run locally — the desktop app's timer and
  your agent both live on your computer, so nothing fires while it is shut down or
  asleep. Event-driven playbooks catch up (the events wait in the queue and the first
  pass after wake leases them) and `interval:` playbooks resume on their next tick,
  but an `at:` time that passes while the machine is off is **skipped** — it does not
  run late.
</Note>

## Two kinds of trigger

Every playbook sets exactly one of `on.event` or `on.schedule` — that choice is what
makes it event-driven or scheduled.

**Event-driven** (`on: { event: <type> }`) binds the playbook to an ack-queue event
type. A run leases pending events of that type, renders the prompt once per event, and
acknowledges them. The event-type namespace is an open set; two types reach the queue
today — `meeting.transcript_ready` (what `playbooks init` binds to) and
`meeting.initial_summary_ready`. A pass is scoped to one type and never sees the other's
events.

**Scheduled** (`on: { schedule: … }`) fires on a timer and never touches the queue.
There is no triggering event, so the prompt fetches whatever data it needs itself (for
example `timeless meetings upcoming`). Set one of:

* `interval: 30m` — a positive Go duration.
* `at: ["09:00"]` — one or more `HH:MM` 24-hour local times, optionally narrowed with
  `weekdays: [mon, tue, wed, thu, fri]`.

<Note>
  Timing lives **inline**, in the playbook. There is no `timeless schedule` command and
  no `schedule.yaml`: the desktop app's resident timer derives what to run, and when,
  from the `playbooks:` map directly. Seeding a playbook is scheduling it.

  An event-driven playbook is polled by the app on a built-in **5-minute** cadence.
  Override it per playbook with `on.every: 1m`. An **empty** `playbooks:` map runs
  nothing — there is no implicit default.
</Note>

## Where they live

Playbooks live under the `playbooks:` key in your config file at
`~/.config/timeless/config.yaml` (respects `TIMELESS_CONFIG_DIR` / `XDG_CONFIG_HOME`).
Run [`timeless playbooks init`](/cli/commands#playbooks) to seed the defaults — it is
idempotent and never overwrites a prompt you have edited. On a fresh machine it
installs `transcript_notes`. `timeless playbooks list --json` shows what's configured
and reports each trigger as `event=<type>` or `schedule=…`.

```yaml theme={null}
# ~/.config/timeless/config.yaml
harness: claude-code            # default harness for every playbook
harness_model: claude-sonnet-5  # default model (claude-code only)

playbooks:
  transcript_notes:             # the key is the playbook NAME
    on:
      event: meeting.transcript_ready
    prompt: |
      A meeting transcript is ready (event {{.EventType}}).
      1. Fetch it: run `timeless meetings transcript {{.MeetingID}}`.
      2. Write a self-contained HTML recap to {{.OutPath}}.

  weekly_brief:                 # a scheduled playbook — no event, no queue
    on:
      schedule:
        at: ["09:00"]
        weekdays: [mon]
    prompt: |
      Summarize last week's meetings as of {{.Today}}.
      Write a self-contained HTML brief to {{.OutPath}}.
```

Each playbook accepts:

| Field     | Purpose                                                                                                                               |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `on`      | The trigger: `event` (+ optional `every`) **or** `schedule`. Required.                                                                |
| `prompt`  | The instruction handed to the harness (a Go text/template).                                                                           |
| `harness` | Override the harness for this playbook only.                                                                                          |
| `model`   | Override the model for this playbook only (`claude-code` only).                                                                       |
| `enabled` | `false` parks it without deleting it; absent means enabled. Prefer `timeless playbooks disable` / `enable` over editing this by hand. |

## Prompt variables differ by kind

The prompt is a Go text/template. The variable set depends on the trigger, so a
scheduled prompt that references an event-only variable fails at run time.

**Event-driven:**

* `{{.EventID}}` — the queue event id
* `{{.EventType}}` — e.g. `meeting.transcript_ready`
* `{{.MeetingID}}` — the payload's `meeting_id`, falling back to its `id` (which is the
  key real payloads use); empty only if the payload carries neither
* `{{.Payload}}` — the full event payload as compact JSON
* `{{.OutPath}}` — the absolute path the CLI expects the artifact written to

**Scheduled:**

* `{{.Today}}` — the run's local date, `2006-01-02`
* `{{.Now}}` — the run's local timestamp, RFC3339
* `{{.OutPath}}` — as above

<Note>
  A run succeeds when a **non-empty file exists at `{{.OutPath}}`** — the harness's exit
  code is ignored. Every prompt should end by writing there. Artifacts land in
  `~/.local/share/timeless/artifacts` and the desktop app surfaces each one to you
  in-app; the CLI never opens a browser, so keep them self-contained.
</Note>

The path is CLI-owned — always use `{{.OutPath}}` rather than composing a filename. It
takes one of two shapes: `<event type>-<event id>-<playbook>.html` for an event-driven
run (the playbook name is in there precisely so several playbooks on one type cannot
overwrite each other) and `<playbook>-<YYYY-MM-DD>.html` for a scheduled one, where
there is no event id to key on.

<Warning>
  **When a prompt cannot do its job, it must write nothing.** The CLI never reads the
  file's contents, so a document explaining what went wrong is a *successful* artifact as
  far as it can tell — and the app delivers it to you as your finished recap. Leave
  `{{.OutPath}}` absent and print the reason instead; the attempt is then recorded as a
  `no_artifact` loss and the app can tell you that deliverable failed.
</Warning>

## Run them

```bash theme={null}
timeless playbooks run --event-type meeting.transcript_ready   # event-driven
timeless playbooks run weekly_brief                            # scheduled, by name
```

The unit of an event-driven pass is the **event type**, not a playbook. A name may only
be used for a scheduled playbook; naming an event-driven one is refused, because two
invocations racing for one event is exactly what `--event-type` prevents.

<Warning>
  Delivery is **at-most-once**: each event is acknowledged once, *before* any attempt.
  An event gets one round of attempts, and one that produces nothing is reported in
  `losses` rather than retried. A failed run is **not** redelivered on the next tick.
</Warning>

## Stale events are skipped, not run

An event whose meeting time is older than
[`events.max_age`](/cli/config#freshness-window) (default `24h`) is **acknowledged and
skipped**: consumed for good, but no playbook runs and no artifact is written. It is
reported as `expired` and the pass still exits 0 — a skip is not a failure and never
means a recap went missing. This is what stops three days away from producing a burst of
dozens of recaps for meetings you have moved on from.

Widen the window when you want a backlog processed on purpose — `--max-age 168h` for one
pass, or `timeless config set events.max_age 168h` to persist it. There is no undo once
an event has been retired.

## Turn one off without deleting it

```bash theme={null}
timeless playbooks disable weekly_brief   # stop it running on future triggers
timeless playbooks enable weekly_brief    # let it run again
```

These write the playbook's `enabled` key. Nothing is deleted — the prompt and trigger
survive, so `enable` restores it exactly. Both are idempotent, and they govern **future**
runs only: a pass already in flight still finishes, so one more recap can land right
after you disable something. That run already consumed its event; let it land.

<Note>
  `playbooks list` does **not** show enabled state — it reports name, trigger, harness,
  and prompt only. Read `config.yaml`, or trust the `changed` field `disable` returns.
</Note>

## Several playbooks can share one event type

This is the point most often gotten wrong, and older guidance said the opposite.
Sharing a trigger is **correct** — it is the supported way to get several deliverables
from one meeting.

`timeless playbooks run --event-type <type>` acknowledges each event once, then runs
**every** enabled playbook bound to that type, in name-sorted order, each writing its
own artifact. Paths are keyed `<event type>-<event id>-<playbook name>.html`, so they
cannot collide. Add a CRM sync and a Slack digest alongside `transcript_notes` — all
three on `meeting.transcript_ready` — and one transcript produces three artifacts.
Nothing needs to be renamed or removed to make room.

Two related guarantees: [`events pull`](/cli/commands#events) is read-only and can
never cost an event its attempt, and scheduled playbooks never touch the queue at all.

<Note>
  Each playbook is a full agent run and they execute **sequentially**, so cost and
  wall-clock grow with the size of the set for one event type.
</Note>

## Let your agent customize them

There is no `timeless playbook` command — playbooks are just YAML, so you edit them
directly or ask your agent to. The agent already knows the file location and the
template contract, so you can describe the change in plain language:

* **"Explain my playbooks"** — the agent reads the config and summarizes each one.
* **"Have the recap include action items with owners"** — the agent edits the `prompt`
  in place, preserving the template variables and the write-to-`{{.OutPath}}` step.
* **"Poll for transcripts every minute"** — the agent sets `on.every: 1m`.
* **"Pause the weekly brief"** — the agent runs `timeless playbooks disable weekly_brief`.
* **"Run transcript recaps on Codex"** — the agent sets `harness: codex` on that
  playbook.
* **"Add a Monday-morning digest"** — the agent adds a playbook with an
  `on.schedule` trigger.

After a change, run `timeless playbooks list --json` to confirm the trigger parsed as
you intended, then exercise it with `timeless playbooks run`.

<Warning>
  When you edit a prompt, keep the instruction to write a complete, self-contained HTML
  document to `{{.OutPath}}`. Drop it and the run produces no artifact — and because
  delivery is at-most-once, that event is reported as a loss rather than retried.
</Warning>
