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.
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:
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.
`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.
-`--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:
`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.
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.