Skip to main content
Every command group prints help on a bare invocation (timeless meetings, timeless bot, …) and accepts --json. Because the CLI is agentic-first, output is JSON automatically when piped; pass --json to force it anywhere. timeless <group> --help is always authoritative for the full flag list.

Skills

Write the embedded skill bundle into your coding agent’s discovery layout so it knows how to drive the CLI. This is the CLI-side step of install — the desktop app installs the binary and owns login; you (or your agent) install the skill.
--harness accepts claude-code, codex, or cursor, is repeatable to wire several at once, and defaults to the harness in your config. The result lists every file written per harness.
skills install writes files only — it seeds no config. Setting up the playbooks: map is timeless playbooks init. If playbooks list comes back empty, skills install will not fix it; the two are separate steps of onboarding.
There is no timeless setup command in the desktop-app model — the app handles install and login; skills install handles the agent wiring, and playbooks init seeds the default playbooks.

Auth

The CLI authenticates with OAuth (a loopback PKCE flow), not an API token — unlike the REST API, which uses a Bearer token. You never paste a token into the CLI: you sign in through the browser once, and the token is stored in your OS keychain (with an encrypted file fallback under the config directory). Every authenticated command resolves its token from that store.
status is a state report, not an identity lookup — there is no whoami endpoint. It exits non-zero when you are not logged in, even though it still prints a clean JSON document, so judge the connection by the exit code. Use --wait instead of a sleep loop when something else is performing the sign-in — a human in the desktop app window, or the app provisioning a token; it exits 0 the moment the token is valid, or non-zero at the deadline. --wait is not a check. Reach for the plain status --json to find out whether you are authenticated, and add --wait only once a sign-in is already under way — otherwise you block for the full budget to learn something the instant check would have told you. The desktop app and the bundled CLI share one token, so signing in once covers both. If the app is already signed in, it can provision the CLI’s token from that session with no browser at all. That session is the whole input: if the app is not signed in there is nothing to provision from, and the trigger still reports emitted: true while the wait after it simply expires. app authenticate returns immediately, so pair it with a short --wait:
Conversely, timeless login writes to the same shared store the app reads, so the app can refresh the token going forward.
When any command returns a re-login error, the fix is always timeless login. A 401 from the API surfaces this way.

Meetings

Read meetings, transcripts, and recordings.
list supports --status (completed|processing|scheduled|failed), --start-date / --end-date (YYYY-MM-DD), --search, --participant, --company, --room, --limit (1–100), and --cursor for pagination. Paginate by passing the previous response’s next_cursor back as --cursor while has_more is true. upcoming lists not-yet-started events from your connected calendar and accepts --within-hours (only events starting within that window), --limit (1–100), and --cursor.
upcoming returns calendar events whose IDs are evt_ IDs — exactly what timeless bot schedule <event-id> expects. Already-recorded meetings carry mtg_ IDs and are returned by meetings list instead.

Bot

Control the AI note-taker. <event-id> is a calendar event ID, not a meeting ID. Scheduling and cancelling are asynchronous.

Events

Drain the delivery queue directly — the building block for custom recap delivery.
Each event is { "id", "type", "created_at", "payload": { … } }. event.type is an open set — dedupe by the stable event.id and skip types you do not handle. Two types reach the queue today: meeting.transcript_ready and meeting.initial_summary_ready.
events pull is read-only: it can display events but never consume them. Delivery uses an SQS-style visibility timeout, so a leased event reappears once the timeout lapses — inspecting an event, any number of times, never costs it its attempt. (--ack was removed and now errors; only playbooks run --event-type consumes events.)

Playbooks run

Turn events into artifacts. This is the sole entrypoint that executes a playbook, in two mutually exclusive forms:
--event-type leases pending events of that type and runs every enabled playbook whose on.event matches, one attempt each, each writing its own artifact — so three playbooks on one type turn one meeting into three deliverables. A <name> may only be used for a scheduled playbook, which renders its prompt once with time variables and writes a single artifact without touching the queue. Naming an event-driven playbook is refused, because two invocations racing for one event is exactly what --event-type prevents. --limit and --max-age are event-form only and inert with a playbook name. --max-age overrides events.max_age for one pass — 0 disables expiry, 168h widens it to a week — which is how you deliberately work through a backlog. Artifacts are written to ~/.local/share/timeless/artifacts; the desktop app surfaces each one in-app. run never opens a browser. The result reports counts at two levels, which is the easy thing to get wrong. pulled, delivered, deferred, and expired are per event; attempts, handled, and lost are per (event, playbook) pair. One event with three playbooks gives delivered: 1 and attempts: 3, so delivered == handled + lost is false — compare outcomes against attempts. handled is the success count; lost entries are listed in losses[] with a reason. Exit status is non-zero when lost > 0 or deferred > 0.
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. Do not expect a failed run to be redelivered.
expired is not a failure. An event whose meeting is older than events.max_age is acknowledged and skipped — no playbook runs, no artifact is written, and the pass still exits 0. That is what stops a week away from producing a burst of stale recaps. deferred does make the exit non-zero, but it is not a lost deliverable: the lease succeeded and the acknowledgement did not, so the event was never attempted and is still queued for a later run. Reporting one as a loss would be a false alarm.

App

Drive the Timeless desktop app, which owns install readiness and authentication. These commands talk to the running app over its control channel.
app ready is the install gate: it exits 0 only when the app is installed, running, and past first-run config. Branch on its ready field — the sibling app_installed is a supplementary on-disk check that reads false on every platform where bundle detection isn’t implemented (currently everything but macOS), so it never means “not installed”. authenticate and sync are fire-and-forget triggers, so emitted: true means the trigger fired, not that the app acted on it. Follow app authenticate with timeless status --wait 2m.

Playbooks

A playbook is keyed by name and carries its own trigger in an on: block — either an event binding or a schedule (see Playbooks). Seed the embedded defaults and inspect what’s configured:
playbooks init is idempotent and never overwrites a prompt you have edited. On a fresh machine it installs transcript_notes, bound to meeting.transcript_ready. It is a pure config write — no binary resolution, no network. Its --harness flag doubles as “set the global default engine”, so seeding and choosing an engine is one step; that also means changed: true with an empty added is normal (the file was rewritten only to record the harness). list reports each playbook’s name, harness, prompt, and trigger as event=<type> or schedule=interval:… / schedule=at:…. disable / enable write one playbook’s enabled key. Nothing is deleted, so enable restores it exactly — never delete a playbook to stop it. Both are idempotent and report {"playbook": …, "enabled": …, "changed": …}; changed: false is a success, not a no-op you need to retry. They govern future runs and do not abort a pass already in flight, so one more recap can still land right after.
playbooks list does not report enabled state — its fields are name, trigger, harness, and prompt only. Trust disable’s own changed field, or read config.yaml, to confirm a playbook is parked.
There is no separate scheduling command and no timeless schedule: each playbook’s cadence lives inline in its on: block, and the desktop app’s resident timer reads the playbooks: map directly. The recurring work is the app’s — it runs while the machine is on and you’re signed in, so you never install a scheduler yourself.

Config

Read and write the scalar keys in config.yaml (see Configuration):
There are exactly seven settable keys, and config list is the authority: events.max_age, harness, harness_model, output, and the three update.* keys — which are inert, accepted and validated but wired to nothing (see Configuration). config get returns the effective value (env + file + defaults), not the file’s text. base_url is env-driven via TIMELESS_BASE_URL, and harness_path is hand-edited only; neither is settable here. Playbooks and logging are nested, not scalar — edit them in config.yaml, or seed the playbooks with playbooks init.

Harness

Report which coding agents this machine can actually launch, and how.
You do not normally need this. It exists for the desktop app’s artifact viewer, whose “Edit with AI” menu has to know what will launch here. It is read-only — no config write, no subprocess, no network. If you just want the configured harness, config get harness is the direct answer. Always three entries in a stable order, including agents that are not installed, so a caller can tell “unsupported” from “supported but missing”. Gate on available (the OR of app_available, a desktop app, and cli_available, a terminal session) — those two differ in kind per harness. All three unavailable still exits 0 with a complete document, so check for an available: true rather than reading exit 0 as “there is something to show”.

Version

timeless --version is equivalent. The CLI does not update itself — the desktop app’s updater owns that.