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|@yearlyalso accepted).@everyandCRON_TZ=/TZ=prefixes are rejected (400). Evaluated intz(IANA), so DST is handled by the zone.nextRunAt: next fire (UTC); null whilepaused.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 theiroriginThreadId.
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 spawnedTaskon 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
- Spawned tasks are kept (their
scheduleIdbecomes 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
backlogtask withstartAt <= nowis launched (link runner path,startAtcleared); thelaunchingevent detail readsscheduled startorscheduled start, late by 12mwhen more than a minute late (e.g. server was down: late starts are not skipped). Manual Start now also clearsstartAt. - Recurring, per unpaused schedule with
nextRunAt <= now, in one transaction (row locked):now - nextRunAt > 2m(server was down): record onemissedrun (detail:missed N fires), advancenextRunAtto the first fire after now. Nothing starts.- A previous spawned task still
backlog(pending launch),launching,runningorneeds_input: recordskipped(detail:#41 still running), advance.donebut unsettled does not block. - Else spawn a task (config + images copied,
scheduleId,startAt = fireAt,startTz = tz), recordstarted, 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 (nextRunAtadvances in the same transaction). - Scheduled starts and skips do not notify; a failed launch notifies as usual (
failed).