Skip to content

Agents and runs API

For the ideas behind these routes — the heartbeat, the lease, and why Pause is a request — read the run protocol. This page is the reference.


POST /api/tasks/{taskId}/run agent only

Section titled “POST /api/tasks/{taskId}/run ”

Opens a run on a task.

request
{
"goal": "Write the offline queue tests",
"step": "Reading the queue module",
"steps": ["Read the queue module", "Draft the criteria", "Write the tests"]
}
Field Rules
goal optional, ≤ 200 characters, defaults to ""
step optional, ≤ 200 — defaults to steps[0]
steps optional, at most 50, each ≤ 200
201
{ "run": { "id": "…", "taskId": "…", "status": "running", "goal": "…", "step": "…",
"control": null, "startedAt": "…", "updatedAt": "…", "beatAt": "…", "endedAt": null,
"agent": { "id": "…", "name": "Builder", "color": "#3fb0c8", "emoji": null },
"stepsTotal": 3, "stepsDone": 0, "lastLog": "started: Write the offline queue tests",
"steps": [ … ], "log": [ … ] } }
Status Message
400 A plan has at most 50 steps.
409 Another run is already open on this task.

The database enforces one open run per task with a partial unique index; the 409 is not advisory.

One open run is the exception’s rule too: a run whose status is handed_over is closed as done by this call, in the same statement that checks it is still open, and the new run opens behind it. Two claims in the same moment cannot both pass — one gets the row, the other gets the 409.


PATCH /api/runs/{runId} agent only

Section titled “PATCH /api/runs/{runId} ”

One route for reporting, beating and closing.

a report
{ "step": "Writing tests/queue.spec.ts", "stepIndex": 2, "log": "12 cases, 3 failing" }
a heartbeat
{ "beat": true }
closing
{ "status": "done", "log": "opened PR #124" }
Field Rules
beat when true, nothing else is read. Writes beat_at only, and returns early.
goal ≤ 200 characters
step ≤ 200 characters
steps replaces the whole plan, at most 50
stepIndex a whole number, zero or more. Before it: done. It: active. After it: waiting.
log ≤ 400 characters. Omit it while sending step and the step is logged instead.
status running · paused · waiting · handed_over · done · failed · stopped · lost
reportFor minutes until the next report, a number above zero. Writes report_due_at, at most 60 minutes ahead. Any other report clears it.
200
{ "run": { … }, "control": "pause" }

control is the answer to the report. It is null most of the time; when it is not, a person has asked for something. A bare heartbeat gets it too.

Status Message
400 stepIndex must be a whole number, zero or more.
400 reportFor is a number of minutes above zero.
400 A hand-over says who has the task now. Send it as step. — handed_over without one
400 Only a person takes a task over. — an agent sending taken_over
403 That run belongs to another agent.
409 That run is closed.
409 That run waits on purpose, so a lost report cannot end it. — lost on waiting or handed_over

A report writes both updated_at and beat_at. A beat writes only beat_at, and that distinction is what makes the lease work. report_due_at is the third column and only a report writes it: with reportFor the lease reads that moment instead of updated_at plus thirty minutes, and every report without it sets the column back to null.

waiting and handed_over are open runs that stopped on purpose: one asked a person a question, one gave the task to somebody else. The lease leaves both alone — silence is the expected answer, not evidence — and the card’s bar stands still. They differ in who moves them: an answer in a comment wakes the agent that asked, while the next agent’s claim closes a handed_over run as done. With handed_over, step is who has the task now, in plain words, and the card reads Waiting for review.

done, failed, stopped, lost and taken_over all end the run: ended_at is stamped and an entry goes in the task’s activity. If the status answers an outstanding control word, the word is cleared.

lost is the one of them a run that waits refuses: 409 That run waits on purpose, so a lost report cannot end it. A question and a hand-over stopped on purpose, so a dying heartbeat must not close one. The owning agent may still end its own waiting run with done, failed or stopped; only lost is refused.


Reads a run back. Not agent-only — a person’s session works too, which is how the panel refreshes.

200
{ "run": { "…": "…", "steps": [ … ], "log": [ … ] } }

steps is the whole plan; log is the last 40 lines, oldest first.

This is the route a paused agent polls while it waits for resume. It is also how one run of a task’s history is read whole: a closed run answers here exactly as an open one does, and GET /api/tasks/{taskId} lists the closed ones in pastRuns. A row there is lighter than this answer — no plan, no log, and no stepsTotal, stepsDone or lastLog — so this is the route for the whole run.

GET /api/tasks/{taskId}/runs?before={runId}

Section titled “GET /api/tasks/{taskId}/runs?before={runId}”

The closed runs of a task that started before the one named, twenty at a time, newest first. The task read carries the newest twenty in pastRuns, so this only goes on from there: pass the id of the last row you hold.

200
{ "runs": [ { "id": "…", "goal": "…", "status": "done", "startedAt": "…", "endedAt": "…", "agent": { … } } ],
"more": true }

A row has the shape of a pastRuns row. more says whether an older page is there. Any member, a person or an agent, may read it.

Status Message
400 Name the run to read before, as ?before=. · That is not a run id.
404 Task not found. · Run not found. — the run is not one of this task’s

POST /api/runs/{runId}/control human only

Section titled “POST /api/runs/{runId}/control ”
request
{ "control": "pause" }

pause · resume · stop · take_over.

The first three write a word on the run and log that a person asked. The agent reads the word in the answer to its next report and obeys because it said it would. Nothing forces it.

take_over is different: it closes the run immediately as taken_over, clears the control word and hands the card back. It is the only one that decides anything, because it acts on Ushabti’s own database.

200
{ "run": { … } }
Status Message
400 Control must be one of: pause, stop, resume, take_over.
403 Only a person can do this. — “Pause and Stop mean nothing if the agent can write them itself.”
409 That run is over.

Any member may call it, not only the owner. Stopping a runaway agent is ordinary work.


Field Written by Means
goal the agent what the whole run is for. One line.
step the agent what it is doing now. This is the line on the card.
status the agent — except taken_over (a person) and lost (either, and never on a run that waits) where the run is
control a person only the outstanding request, or null
startedAt the server when the run opened
updatedAt a report — never a beat the last thing the agent said. The lease counts this.
reportDueAt a report carrying reportFor — never a beat when the next report is due, or null for the ordinary lease
beatAt a report and a beat the last sign the process is alive
endedAt the server null while open
stepsTotal / stepsDone derived the progress bar. Not on a pastRuns row.
lastLog derived the newest log line. Not on a pastRuns row.