Tasks
Shapes
Task:
{
"id": 7,
"projectId": 1,
"projectTitle": "stage",
"title": "Say OK",
"prompt": "reply OK",
"providerInstanceId": "claude",
"model": "claude-haiku-5-5",
"modelOptions": [{ "id": "effort", "value": "low" }],
"runtimeMode": "full-access",
"interactionMode": "default",
"workspace": { "type": "root" },
"tags": ["smoke"],
"notes": "",
"state": "running",
"t3ThreadId": "thread_abc",
"afterTaskId": null,
"nextTaskId": null,
"startAt": null,
"startTz": null,
"scheduleId": null,
"scheduleTitle": null,
"originThreadId": null,
"originThreadTitle": null,
"afterThreadId": null,
"afterThreadTitle": null,
"attachments": [{ "id": 4, "name": "error.png", "mimeType": "image/png", "sizeBytes": 48211 }],
"launchAttempt": 1,
"lastError": null,
"t3WebUrl": "http://t3.example.com:3773/env_123/thread_abc",
"t3AppUrl": "https://app.t3.codes/threads/env_123/thread_abc",
"launchedAt": "2026-11-08T10:00:00Z",
"finishedAt": null,
"settledAt": null,
"runStartedAt": null,
"runEndedAt": null,
"lastWordPreview": null,
"lastWordAt": null,
"pullRequests": [],
"pendingQuestionCount": 0,
"createdAt": "2026-11-08T09:59:00Z",
"updatedAt": "2026-11-08T10:00:00Z"
}providerInstanceId/modelnull = T3 default; they must be set together (400 otherwise).modelOptionsdefault[].afterTaskId: runs after that task (launches when it is settled; see Link rules).nextTaskId: the task that runs after this one. Both nullable.startAt(UTC instant) /startTz(IANA zone it was entered in): one-shot start time of abacklogtask; the scheduler launches it at that time (see Schedule rules). Both null or both set; non-null only while the task is inbacklog(any launch clears them). Never set together withafterTaskId.scheduleId/scheduleTitle: the recurring schedule that spawned this task (null for ordinary tasks, or once the schedule is deleted).originThreadId/originThreadTitle: the T3 session that created the task (agent CLI/MCP provenance; spawned schedule tasks inherit the schedule's). Title is the mirrored thread title, null when not mirrored. Not editable.afterThreadId/afterThreadTitle: runs after that T3 session (launches when it is settled; see Link rules). A task waits on at most one ofstartAt,afterTaskId,afterThreadId.handoff: append the settled predecessor's handoff block to the launch prompt (see Handoff). Defaulttrue; editable inbacklogonly.attachments: the task's images (Attachment, upload order), sent with the prompt on every launch. Default[]. See Images.t3WebUrl=T3_PUBLIC_URL/<environmentId>/<t3ThreadId>(null without thread or environment id).t3AppUrl= T3 threadlinkas served to external MCP clients (https://app.t3.codes/threads/<env>/<thread>; null without thread).finishedAt: first entry intodone|failed|interrupted|cancelled(per launch attempt).settledAt: entry intosettled.runStartedAt/runEndedAt: the task thread's elapsed run span (see Thread); null without a thread or before a detail read.lastWordPreview/lastWordAt: the task thread's last word preview and reply time (see Thread); null without a thread or before one is read.pullRequests: the task thread'sPullRequest[](see Thread);[]without a thread.pendingQuestionCount: the task thread's answerable pending questions (see Thread); 0 without a thread.
TaskEvent:
{ "id": 3, "taskId": 7, "fromState": "launching", "toState": "running", "detail": "thread status running", "at": "2026-11-08T10:00:01Z" }fromState null for the creation event.
TaskDetail = Task + { "events": TaskEvent[] (oldest first), "thread": Thread | null, "sequence": Task[], "batch": CreatedTasks | null, "currentHandoff": Handoff | null, "currentLaunchBase": LaunchBase | null }.
currentHandoff: the current launch attempt's handoff record (see Handoff); null before the first launch, for unlinked tasks, or withhandoffoff.currentLaunchBase: the worktree base the current launch attempt used (see Predecessor base); null before the first launch or withoutworkspace.basepredecessor.sequence: see Link rules.batch: the tasks the task's origin session created (see Session batch); null whenoriginThreadIdis null.thread.lastWordcarries the reply excerpt;thread.recentRunsthe runs.
GET /api/tasks
Query (all optional): project (project id), state (repeatable TaskState), tag (has tag), q (case-insensitive substring of title or prompt), from, to (createdAt), settledFrom (settledAt >= settledFrom).
200 { "tasks": Task[] } ordered by updatedAt desc.
POST /api/tasks
Body CreateTaskRequest:
{
"projectId": 1, // required
"title": "Say OK", // required
"prompt": "reply OK", // required
"providerInstanceId": "claude",// optional, null
"model": "claude-haiku-5-5", // optional, null
"modelOptions": [{ "id": "effort", "value": "low" }], // optional, default []
"runtimeMode": "full-access", // optional
"interactionMode": "default", // optional
"workspace": { "type": "root" },// optional
"tags": [], // optional
"notes": "", // optional
"afterTaskId": null, // optional: run after this task (link rules)
"afterThreadId": null, // optional: run after this T3 session (link rules); not with afterTaskId
"originThreadId": null, // optional: T3 session creating the task (provenance)
"attachmentIds": [4], // optional: staged uploads to attach (max 8)
"startLocal": null, // optional: one-shot start, wall time "YYYY-MM-DDTHH:mm" in startTz
"startTz": null, // required with startLocal: IANA zone, e.g. "America/New_York"
"handoff": true, // optional, default true: append the predecessor handoff at launch
"start": false // optional: true = Save & start (ignored when afterTaskId or afterThreadId set)
}201 Task (state backlog, or launching when started).
400, including: an attachmentIds entry that is not a staged upload, more than 8; startLocal without startTz or vice versa; unknown zone; unparseable startLocal; a start time not in the future; startLocal together with afterTaskId, afterThreadId or start: true; afterTaskId together with afterThreadId.
404 project / afterTaskId / afterThreadId (session not mirrored). 409 link rule broken. Start errors after save: task is still created; it lands in failed with lastError.
GET /api/tasks/:id
200 TaskDetail. 404.
PATCH /api/tasks/:id
Body UpdateTaskRequest: any of projectId title prompt providerInstanceId model modelOptions runtimeMode interactionMode workspace tags notes afterTaskId afterThreadId attachmentIds startLocal startTz.
- In
backlogall fields are editable; otherwise onlytitle,tags,notes(409 for others). attachmentIdsis the exact image set: the task's own ids to keep plus staged uploads to add; the task's images not listed are deleted (400 for ids that are neither, more than 8).afterTaskId: set links per Link rules (409 / 404);nullunlinks (any state). "Then run" on task A =PATCHthe follower withafterTaskId: A.startLocal+startTz(backlog only, 409 otherwise) set the one-shot start (same 400 rules as create); bothnullclears it.- The resulting task must wait on at most one of a start time,
afterTaskId,afterThreadId(400); switch in one request (e.g.{ "startLocal": null, "startTz": null, "afterTaskId": 12 },{ "afterTaskId": null, "afterThreadId": "thread-x" }). afterThreadId: set links per Link rules;nullunlinks.handoff:true|false(backlog only, 409 otherwise; 400 fornull).
200 Task. 404.
POST /api/tasks/order
Body { "taskIds": number[] }: link the tasks to run in that order (each after the previous one), in one transaction. Rules in Session batch.
200 { "tasks": Task[] }: the resulting run order, head first (including linked tasks outside the list). 400 fewer than two ids or a duplicate id. 404 missing task. 409 a listed task is not backlog, or a link rule is broken (nothing changes).
DELETE /api/tasks/:id
- 409 when state is
launching|running|needs_input(stop first). Its follower is relinked to its predecessor (A→X→B becomes A→B; B unlinked when X had none).
POST /api/tasks/:id/start
Launch in T3 (t3_thread_launch, clientRequestId = "task-<id>-<uuid>", new attempt, new clientRequestId and new thread each start). T3 dedupes launches by clientRequestId across clients; the random part keeps other databases (dev, tests, restored backups) on the same T3 from colliding. A launch answered with a thread in another project than the task's is a collision: the task moves to failed without adopting the thread.
Allowed from backlog|failed|interrupted|cancelled; else 409. "Start now": a backlog task waiting on a predecessor task or session that is not settled is unlinked from it first (its own follower stays linked). Each start clears t3ThreadId, lastError, finishedAt, settledAt; the previous thread stays in history (as source: "t3").
A transport failure (timeout, network, dropped session; not a T3 tool error) is retried once with the same clientRequestId. If that also fails the task moves to failed and the next start reuses the attempt (same launchAttempt and clientRequestId), so T3 returns the thread if the lost launch did create one. The same applies to a task left in launching by a server restart (failed at startup with launch interrupted by server restart). On shutdown the server waits up to 100s for in-flight launches (compose stop_grace_period: 2m).
Images: before each launch every task image is uploaded to T3 afresh (t3_attachment_prepare_upload, then the bytes POSTed to the signed relativeUrl on T3_URL) and passed as attachments to t3_thread_launch. A failed upload or launch fails the start (lastError) and discards that start's uploads (best effort).
200 Task (state launching or later). 502 / 503 (task moves to failed with lastError on T3 error).
POST /api/tasks/:id/followup
Body { "message": string, "mode"?: SendMode, "attachmentIds"?: number[] }.
- Without images:
t3_thread_send(message required). - With
attachmentIds(staged uploads, max 8): the images are uploaded to T3 and sent with the optional message as one new message (t3_thread_send_attachments);modeis not allowed (400). Sent images are deleted; on a T3 error they stay staged (no automatic retry).
409 when task has no thread. 200 Task. 400. 502 / 503.
POST /api/tasks/:id/stop
t3_thread_interrupt (stop). State updates via poller. 409 when task has no thread.
Follow-up and stop bump updatedAt; that re-arms polling of a failed|interrupted task for 10 minutes (see Polling scope). They then poll the thread once: the thread mirror is refreshed, but task state does not change for backlog tasks or settled/cancelled tasks that have a follower (one without may reopen).
200 Task. 502 / 503.
POST /api/tasks/:id/duplicate
New backlog task with the same project, title, prompt, images (copied), model config, workspace, tags, notes; not linked, no start time, no schedule.
201 Task. 404.
State mapping
Thread → task, a pure function. Input: T3 thread detail (status, settled, pendingRequestCount) and the latest run status (recentRuns[0].status, may be absent). First matching rule wins:
| # | Condition | Task state |
|---|---|---|
| 1 | settled == true | settled |
| 2 | pendingRequestCount > 0 | needs_input |
| 3 | status in (preparing, queued, starting, running, waiting) | running |
| 4 | status == completed | done |
| 5 | status == failed | failed |
| 6 | status == interrupted | interrupted |
| 7 | status in (cancelled, rolled_back) | cancelled |
| 8 | status == idle and latest run status present | apply rules 3-7 to the latest run status |
| 9 | otherwise (idle, no runs) | unchanged (keep current state) |
T3 waiting is the checkpoint gap after a provider turn ends (T3 shows it as "Working"), not a wait on the user; a pending approval or question keeps the thread running with pendingRequestCount > 0. T3's UI "Waiting" (blocked on a child thread, or awaited background work such as a PR watch after the turn ended) is not exposed over MCP: those threads report running or completed and map to running or done.
Applied only to tasks with a thread whose state is not backlog, and not to settled or cancelled tasks with a follower (it has been released). T3 reports a cancelled turn (e.g. a denied permission) as thread status cancelled and the thread can run on, so a cancelled task without a follower reopens like a settled one. A change writes a task_events row (detail = observed T3 status) and sets finishedAt / settledAt as above.
Polling scope
Every 5s, t3_thread_read:
- tasks with a thread in
launching|running|needs_input; donetasks updated in the last hour (older ones at most every 60s);failed|interrupted|cancelledtasks updated in the last 10 minutes or whose thread mirror (full sync, every 5 min) shows the thread unsettled and active (preparing|queued|starting|running|waitingor pending requests); acancelledtask with a follower is excluded.
settled is not polled. A thread T3 no longer knows moves the task to failed (lastError "thread not found in T3"). Tasks left in launching by a server restart move to failed on startup.
Link rules
- A task runs after at most one task (
afterTaskId) and has at most one follower (nextTaskId): a run order is a linear list. Links may span projects. - Linking (create or
PATCH afterTaskId), 409 otherwise: the task isbacklog; the predecessor exists (404), is not the task, is notsettled, has no other follower, and is not already after the task (no loops). - A predecessor is passed when it is
settled. The link runner launches everybacklogtask whose predecessor has passed: on each transition intosettled, at startup and every 10s. The link stays (history). - A
failed|interrupted|cancelled(or still running /done) predecessor keeps its follower waiting. Restart the predecessor, unlink the follower, or "Start now" on it. GET /api/tasks/:idreturnssequence: the whole run order containing the task, head first.
Session links (afterThreadId, "run after me" from the agent interface):
- Linking a session that is a task's current thread stores a task link to that task instead (
afterTaskId, all rules above, so 409 when it already has a follower). - Otherwise (409 / 404): the task is
backlog; the session is mirrored (404) and has not passed. Many tasks may wait on one session. - A session is passed when it is settled in T3 with no pending requests and no active run (the raw settled flag: a settled session whose last run was cancelled passes). The link runner launches every
backlogtask whose session has passed; failed, interrupted, cancelled, running or done-but-unsettled keep it waiting. The watcher refreshes every session that a backlog task waits on each 5s pass (subagents included, no notifications), and a session settling kicks the runner. The link stays (history); it is not part ofsequence.
Handoff
When a linked task launches (link runner, Start, or restart from failed|interrupted|cancelled) with handoff on and its predecessor passed (a settled task with a thread, or a passed session), stage reads the predecessor from T3 (t3_thread_read and its latest assistant reply, 20s budget) and sends prompt + "\n\n" + block:
<handoff from="stage">
This task runs after "Build API" (task #12), settled 2026-11-14T10:02:00Z.
- T3 thread: <threadId> <link> (read more with t3_thread_read)
- Branch: feat/api · Worktree: /path/to/worktree
- Pull requests:
- owner/repo#41 (open) https://github.com/owner/repo/pull/41
Final reply:
<the predecessor's final assistant reply, at most 4000 chars>
[truncated at 4000 chars; full reply via t3_thread_read <threadId>]
</handoff>(session)replaces(task #12)for session links. Empty branch/worktree and PR lines are omitted; no reply givesFinal reply: none found.; the truncation line appears only when the reply is longer.- Composed at launch, never at link time. Each launch attempt records
Handoffintask_handoffsplus alaunchingtask event (handoff from #12 appended (N chars)). An attempt that reuses its clientRequestId (lost launch response) resends the stored text. - A failed read never blocks the launch: the task launches with the bare prompt, the attempt is recorded as
skippedwithreason, and the event readshandoff skipped: <reason>. - "Start now" on a task whose predecessor has not passed unlinks it first, so no handoff is sent.
Handoff:
{ "attempt": 1, "status": "sent", // sent | skipped
"sourceTaskId": 12, // null for a session link
"sourceThreadId": "thread-abc", "sourceTitle": "Build API",
"text": "<handoff ...", // "" when skipped
"reason": null, // why skipped
"at": "2026-11-14T10:02:01Z" }Predecessor base
A task with workspace.base predecessor (opt-in per task, default ref) starts its T3 worktree from its predecessor's branch, so stacked sequential work builds on unmerged changes. Resolved at launch (link runner, Start, restart), from the same live t3_thread_read as the handoff:
- Uses the predecessor's local branch:
{type:"worktree", baseRef: <branch>, startFromOrigin: false, branch: <the task's branch, else T3 picks>}. Worktrees share refs, so the predecessor's worktree branch resolves in the project repo, unpushed commits included. - Falls back to the task's own
baseRef(samestartFromOriginrule asWorkspace) when, in order: the task is not linked; the predecessor has not passed or has no thread; the read fails; the predecessor ran in another T3 project; it ran at project root (no worktree); it has no branch; it has pull requests and all aremerged(its work is already on the default branch). - T3 rejects a base ref that does not resolve before creating a thread (
invalid_request,Base ref "<branch>" does not resolve…; e.g. the branch was deleted). stage then relaunches once from the fallback with the same clientRequestId and records reasonbranch <x> not found in project. - The launch is never blocked by the base. Each attempt records
LaunchBaseintask_launch_basesplus alaunchingtask event:base from #12's branch feat/api(session "Title"'sfor session links) orbase fallback origin/main: <reason>. An attempt that reuses its clientRequestId resends the stored base. - The follower's own branch name is unchanged (
workspace.branch, else T3 picks). A follower branch equal to the predecessor's fails in T3 (already checked out).
LaunchBase:
{ "attempt": 1, "baseRef": "feat/api", "startFromOrigin": false,
"source": "predecessor", // predecessor | fallback
"sourceTaskId": 12, // null for a session link or when not linked
"sourceThreadId": "thread-abc", // null when not linked
"sourceTitle": "Build API",
"reason": null, // why it fell back
"at": "2026-11-14T10:02:01Z" }Session batch
A session batch is the set of tasks one session created (originThreadId), usually a plan an agent broke into tasks.
CreatedTasks:
{
"chains": [[Task, Task, Task]], // runs of batch tasks linked by afterTaskId, head first; ordered by head id
"independent": [Task] // batch tasks with no batch predecessor or follower, by id
}- A chain stops at the batch boundary: a batch task whose predecessor is outside the batch heads its chain (or is independent) and keeps its
afterTaskId; an outside follower shows as the last step'snextTaskId. - Starting a chain =
POST /api/tasks/:id/starton its head; followers already wait. A head waiting on a task or session is unlinked first ("Start now").
POST /api/tasks/order (order_tasks, stage task order) rules:
- At least two distinct tasks (400), all
backlog(409). - Links between listed tasks are replaced;
taskIds[i]runs aftertaskIds[i-1]. Followers lose their start time and session wait. - The first task keeps its own wait (start time, session, or a task outside the list).
- Reorder: when the listed tasks already form exactly one chain, a new head without a wait takes the old head's wait, and the old tail's follower outside the list moves to the new tail.
- Each link follows Link rules (e.g. 409 when an outside follower of a middle task would give it two followers, or a task belongs to a queued continuation order).