# SF Schedule System — Specification > **Spec version:** 1.0.0 > **Status:** Implemented (M010 S02) > **Owner:** M010 S05 --- ## Overview The SF schedule system provides time-based reminders and deferred work items that surface at a future date. Entries are stored in SQLite (`schedule_entries`) and queried on demand (pull-based), not fired by a daemon or cron job. This makes the system portable, auditable, and free of background processes. Use `sf schedule` when something needs to happen at a specific future time but cannot (or should not) happen immediately: - **Schedule** — time-bound items that must surface on a date, even if SF is not running continuously - **Backlog** — priority-ordered items with no specific timing (SF's standard milestone/slice queue) --- ## Design Rationale ### Pull-Based, Not Daemon-Based SF has no long-running daemon. Entries are not "fired" by a timer. Instead, the schedule store is queried at specific integration points: 1. **On launch** — `loader.ts` calls `findDue()` and prints a banner if items are due 2. **Autonomous mode boundaries** — `sf headless query` (machine snapshot) and the TUI status overlay include due/upcoming entries in their output 3. **CLI query** — `sf schedule list --due` shows items whose `due_at <= now` This means: if an item is scheduled for 3 AM and you open SF at 9 AM, you will see the item as overdue. There is no fire-at-exact-time guarantee. This is an explicit trade-off — see the [pull-based ADR](../adr/0002-sf-schedule-pull-based.md) for the full decision record. ### ULID Identifiers Schedule entries use [ULID](https://github.com/ulid/spec) (Universally Unique Lexicographically Sortable Identifier) instead of UUID. ULIDs are: - 28 characters, Crockford Base32 encoded - Lexicographically sortable by creation time (useful for schedule ordering) - Unique enough to avoid collisions across concurrent appends - Monotonic within millisecond precision via sub-millisecond counter The `generateULID()` function in `schedule-ulid.js` is used for all new entries. ### DB-Primary Ledger Each write appends a row to `schedule_entries`. The latest row per ID wins on read. This means status transitions (`pending` → `done`, `cancelled`, `snoozed`) are implemented as ledger entries, not in-place mutations. Legacy `schedule.jsonl` files are import-only compatibility inputs. Rows without `schemaVersion` are treated as legacy version 1. Unsupported future schema versions are ignored by the current reader. Corrupt lines are skipped with a warning, never fatal. --- ## Storage Format ### Storage Locations | Scope | Path | |-------|------| | `project` | `/.sf/sf.db` | | `global` | `~/.sf/sf.db` with `scope = 'global'` | | legacy import | `/.sf/schedule.jsonl` or `~/.sf/schedule.jsonl` | ### Schema ```json { "schemaVersion": 1, "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", // ULID — 28 chars "kind": "reminder", // ScheduleKind enum "status": "pending", // pending | done | cancelled | snoozed "due_at": "2026-06-15T09:00:00.000Z", // ISO-8601 timestamp "created_at": "2026-05-15T09:00:00.000Z", "snoozed_at": "2026-06-01T09:00:00.000Z", // ISO-8601 — set on each snooze "payload": { "message": "Review adoption metrics" }, // kind-specific "created_by": "user", // user | agent | system "autonomous_dispatch": false // if true + kind=prompt/command, consume from autonomous mode } ``` ### Legacy JSONL Line Example ``` {"schemaVersion":1,"id":"01ARZ3NDEKTSV4RRFFQ69G5FAV","kind":"reminder","status":"pending","due_at":"2026-06-15T09:00:00.000Z","created_at":"2026-05-15T09:00:00.000Z","payload":{"message":"Review adoption metrics"},"created_by":"user","autonomous_dispatch":false} ``` --- ## Schedule Kinds | Kind | Description | Payload fields | |------|-------------|----------------| | `reminder` | General time-based reminder | `message`, `unitId?`, `milestoneId?` | | `milestone_check` | Milestone health check | `milestoneId`, `checkType?` | | `review_due` | Review prompt surfaced at next planning turn | `prUrl?`, `reviewer?`, `unitId?` | | `recurring` | Cron-based recurring entry (future) | `cron`, `unitId?`, `milestoneId?` | | `review` | Alias for `review_due` — same behaviour | — | | `audit` | Audit surfaced at next planning turn | `unitId?` | | `command` | Shell command run by explicit `sf schedule run ` | `command`, `capture?` | `review` and `audit` kinds are surfaced to the next autonomous planning turn (TBD: integration point in `sf_plan_slice` / `sf_plan_task` / autonomous dispatch). They are stored but not autonomous dispatched without a consumer. --- ## CLI Reference All commands are invoked as `/schedule ` in the TUI or `sf schedule ` from the shell. ### `sf schedule add` ``` sf schedule add --in [--kind ] [--scope ] sf schedule add --at <ISO-date> [--kind <kind>] [--scope <scope>] <title> ``` Schedule a new item. **Flags:** - `--in <duration>` — Relative time from now (e.g. `2w`, `30m`, `1d`, `4h`) - `--at <ISO-date>` — Absolute ISO-8601 date - `--kind <kind>` — Entry kind (default: `reminder`). Valid: `reminder`, `milestone_check`, `review_due`, `review`, `audit`, `recurring`, `command` - `--scope <scope>` — `project` (default) or `global` **Examples:** ``` sf schedule add --in 2w "Review feature adoption metrics" sf schedule add --in 30m --kind milestone_check "Check M003 validation" sf schedule add --at 2026-06-01T09:00:00Z --scope global "Team sync" ``` ### `sf schedule list` ``` sf schedule list [--due] [--all] [--json] [--scope <scope>] ``` List scheduled items. **Flags:** - `--due`, `-d` — Show only items whose `due_at <= now` (overdue + just-due) - `--all`, `-a` — Show all entries including `done` and `cancelled` - `--json`, `-j` — Raw JSON output - `--scope <scope>` — `project` (default) or `global` **Output columns:** ID (8-char prefix), Title, Due (relative), Status, Kind **Examples:** ``` sf schedule list sf schedule list --due sf schedule list --all --json sf schedule list --scope global ``` ### `sf schedule done` ``` sf schedule done <id> ``` Mark a pending item as done. ID can be a prefix (ULID prefix match). ``` sf schedule done 01ARZ3ND ``` ### `sf schedule cancel` ``` sf schedule cancel <id> ``` Cancel a scheduled item. ID can be a prefix. ``` sf schedule cancel 01ARZ3ND ``` ### `sf schedule snooze` ``` sf schedule snooze <id> --by <duration> ``` Postpone a scheduled item by a relative duration. Updates `due_at` and sets `snoozed_at`. ``` sf schedule snooze 01ARZ3ND --by 1d sf schedule snooze 01ARZ3ND --by 30m ``` ### `sf schedule run` ``` sf schedule run <id> ``` Execute a scheduled item. For `reminder`, `milestone_check`, `review_due` kinds: displays the title and marks done. For `command` kind: executes the stored shell command and captures output. ``` sf schedule run 01ARZ3ND ``` --- ## Integration Points ### Loader Banner (`loader.ts`) On every SF startup, `loader.ts` calls `findDue()` for both project and global scopes. If any items are due, it prints: ``` [forge] N scheduled item(s) due now. Manage: /schedule list ``` ### Machine Snapshot (`sf headless query`) `headless-query.ts` populates a `schedule` field in `QuerySnapshot`: ```ts schedule: { due: ScheduleEntry[], // due_at <= now upcoming: ScheduleEntry[] // due_at within 7 days } ``` This feeds the `sf status` dashboard and autonomous dispatch context. ### Milestone YAML Schedule (`sf_plan_milestone`) The milestone plan schema supports a `schedule[]` array in the YAML spec: ```yaml schedule: - in: 2w kind: review title: "Review adoption after shipping feature" ``` These entries are created at milestone creation time. The `in` field is relative to `now`. The `on_complete` variant fires a duration after milestone completion. ### Autonomous Dispatch When `autonomous_dispatch: true` and `kind: "prompt"` or `kind: "command"`, the item is consumed by autonomous mode when `due_at <= now`. This is the mechanism for time-bound autonomous repo work. --- ## Duration Format All duration strings follow the format `<number><unit>`: | Unit | Meaning | |------|---------| | `w` | weeks | | `d` | days | | `h` | hours | | `m` | minutes | Examples: `30m`, `4h`, `2d`, `1w` --- ## Examples ### Reminder (2 weeks out) ``` sf schedule add --in 2w "Review feature adoption metrics" ``` ### Milestone Check (at milestone creation via plan YAML) ```yaml # In milestone spec schedule: - in: 2w kind: milestone_check title: "Validate M003 success criteria" ``` ### Audit (surfaced at next planning turn) ``` sf schedule add --in 1mo --kind audit "Audit ADR-007 decision implementation" ``` ### Command (shell command execution) ``` sf schedule add --in 30m --kind command "Reminder: run integration tests" # Note: kind=command requires payload.command field — use the CLI directly # to set kind=reminder for simple reminders ``` ### Global Scope (across all projects) ``` sf schedule add --in 1w --scope global "Review all open milestones" ```