Skip to content
stage

Schedules

A schedule is a recurring task template: on each cron fire it spawns a new task (scheduleId set) with its config and images and starts it. One-shot starts are not schedules; they are startAt on a backlog task (see Tasks).

Shapes

Schedule:

{
  "id": 3,
  "projectId": 1,
  "projectTitle": "stage",
  "title": "Nightly deps bump",
  "prompt": "bump deps, run tests",
  "providerInstanceId": "claude",
  "model": "claude-sonnet-5-5",
  "modelOptions": [],
  "runtimeMode": "full-access",
  "interactionMode": "default",
  "workspace": { "type": "worktree", "baseRef": "origin/main", "branch": null },
  "tags": ["deps"],
  "notes": "",
  "attachments": [],
  "cron": "0 2 * * *",
  "tz": "America/New_York",
  "paused": false,
  "nextRunAt": "2026-11-11T20:30:00Z",
  "lastRunAt": "2026-11-10T20:30:00Z",
  "lastTaskId": 41,
  "lastTaskState": "settled",
  "originThreadId": null,
  "createdAt": "2026-11-08T09:59:00Z",
  "updatedAt": "2026-11-08T10:00:00Z"
}
  • cron: standard 5-field expression (minute hour day-of-month month day-of-week; @hourly|@daily|@weekly|@monthly|@yearly also accepted). @every and CRON_TZ=/TZ= prefixes are rejected (400). Evaluated in tz (IANA), so DST is handled by the zone.
  • nextRunAt: next fire (UTC); null while paused. lastRunAt: fire time of the newest run row (any outcome); null before the first.
  • lastTaskId / lastTaskState: newest task this schedule spawned (null before the first).
  • originThreadId: the T3 session that created the schedule (agent provenance; set on create only). Spawned tasks inherit it as their originThreadId.

ScheduleRun:

{ "id": 9, "scheduleId": 3, "fireAt": "2026-11-10T20:30:00Z", "outcome": "started", "taskId": 41, "detail": "", "at": "2026-11-10T20:30:04Z", "task": null }
  • outcome: started (task spawned; launch result lives on the task), skipped (previous run still active), missed (server was down across the fire time; one row per outage), manual (Run now).
  • task: the spawned Task on detail reads, else null.

ScheduleDetail = Schedule + { "runs": ScheduleRun[] } (newest first, at most 50, each with task).

GET /api/schedules

Query (optional): project (project id), dueWithin (Go duration, e.g. 24h: only unpaused schedules with nextRunAt <= now + dueWithin).

200 { "schedules": Schedule[] } ordered by nextRunAt asc (paused last), then id.

POST /api/schedules

Body CreateScheduleRequest: the CreateTaskRequest config fields (projectId title prompt providerInstanceId model modelOptions runtimeMode interactionMode workspace tags notes attachmentIds originThreadId, same rules; afterTaskId, afterThreadId, start, startLocal, startTz are 400) plus "cron": "0 2 * * *" (required), "tz": "America/New_York" (required), "paused": false (optional).

201 Schedule. 400 (invalid cron or zone, plus task field rules). 404 project.

GET /api/schedules/:id

200 ScheduleDetail. 404.

PATCH /api/schedules/:id

Body: any of the create fields (attachmentIds is the exact image set, as for tasks). Changing cron, tz or paused recomputes nextRunAt from now (resuming never fires a missed time). Spawned tasks are not changed.

200 Schedule. 400. 404.

DELETE /api/schedules/:id

  1. Spawned tasks are kept (their scheduleId becomes null). 404.

POST /api/schedules/:id/run

Run now: spawn a task and start it immediately, ignoring paused and the overlap rule; records a manual run; nextRunAt unchanged. Launch errors land on the task (failed + lastError).

201 Task. 404.

POST /api/schedules/preview

Body { "cron": "0 2 * * 1-5", "tz": "America/New_York" }. 200 { "next": [ISO, ISO, ISO] } (next three fires, UTC). 400 with the parse error message.

Schedule rules

  • The scheduler ticks every 15s (and on start).
  • One-shot: a backlog task with startAt <= now is launched (link runner path, startAt cleared); the launching event detail reads scheduled start or scheduled start, late by 12m when more than a minute late (e.g. server was down: late starts are not skipped). Manual Start now also clears startAt.
  • Recurring, per unpaused schedule with nextRunAt <= now, in one transaction (row locked):
    • now - nextRunAt > 2m (server was down): record one missed run (detail: missed N fires), advance nextRunAt to the first fire after now. Nothing starts.
    • A previous spawned task still backlog (pending launch), launching, running or needs_input: record skipped (detail: #41 still running), advance. done but unsettled does not block.
    • Else spawn a task (config + images copied, scheduleId, startAt = fireAt, startTz = tz), record started, advance. The one-shot pass then launches it (a crash in between still launches it on restart).
  • Fires are computed in the schedule's tz; a fire is never run twice (nextRunAt advances in the same transaction).
  • Scheduled starts and skips do not notify; a failed launch notifies as usual (failed).