Relay

Ask-the-App API reference

The HTTP interface behind the Ask-the-App demo. The assistant proposes; people apply. POST /api/ask never writes to a task. Only apply and undo do, each as one atomic step that re-checks every task first. Every JSON endpoint reports failure in the same envelope (Error) and sends an X-Request-Id header. There is no sign-in: the demo workspace is shared and is reseeded nightly.

Version
0.2.0
Format
OpenAPI 3.1.0, at /api/openapi.json
Base
This site. The examples use $BASE for its address and $ID for a change set id.

Endpoints

MethodPathWhat it does
POST/api/askAsk the assistant for a change
GET/api/changesList change sets
GET/api/changes/{id}Read one change set
POST/api/changes/{id}/applyApply a proposal
POST/api/changes/{id}/undoUndo an applied change set
POST/api/changes/{id}/cancelCancel a proposal
GET/api/boardRead the board
GET/api/peopleList people
GET/api/sprintsList sprints
GET/api/cron/reseedReseed the demo workspace
POST/api/cron/reseedReseed the demo workspace (POST)
GET/api/healthLiveness probe
GET/api/openapi.jsonThis document
GET/api/docsThis reference, as a page

Conventions

Errors

Every JSON endpoint reports failure in one envelope, with the HTTP status set to match. Branch on code; show message to a person. The same requestId is sent as the X-Request-Id header on every response, success or failure, and appears in the server log. Every response also carries Server-Timing: app;dur=<ms>, the time the server spent before it began to answer. An endpoint that reads or writes the database can also answer 500 internal; health and the two documentation endpoints do not list it because they cannot fail that way.

{
  "error": {
    "code": "stale",
    "message": "The board changed since this was proposed. Nothing was applied.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11",
    "conflicts": [
      "..."
    ]
  }
}
CodeStatusMeaning
invalid_request400The request is not valid: bad JSON, too large, a field out of range, or a malformed id.
forbidden_origin403A browser sent a write from another site. Calls without an Origin header are accepted.
unauthorized401The cron route needs its bearer secret.
not_found404No such change set, or no such endpoint.
not_proposed409The change set is no longer a proposal, so it cannot be applied or cancelled. status says what it is.
not_applied409The change set is not in the applied state, so it cannot be undone. status says what it is.
stale409A task changed since the proposal (or the apply). Nothing was written; conflicts lists the rows.
rate_limited429Too many requests from this address. Wait retryAfterSeconds.
budget_exhausted503The assistant spent its daily model budget. resetsAt is the next 00:00 UTC.
unavailable503No model is configured on this deployment.
not_configured503The route is closed because the deployment has no secret for it.
internal500Something failed on the server. Nothing was changed; quote requestId in a report.

Writes

Only apply and undo change a task, and each is atomic: either every change is written or none is. Both re-check every task against what the change set expects, so a person editing the same card is never overwritten silently; the answer is 409 stale with the rows that moved.

Limits

20 asks and 60 other writes per ten minutes per address, 6 cron calls an hour, and a hard daily model budget. A limited call answers 429 with Retry-After.

Assistant

Ask in plain words; get a proposal, a question or a refusal. Never a write.

POST /api/ask

Ask the assistant for a change

Turns a request typed in plain words into a proposal, a question back, a refusal or a short answer. The assistant never changes a task: a proposal is stored as a change set in the proposed state and only POST /api/changes/{id}/apply writes to tasks. The response is a stream of newline-delimited JSON: any number of activity events while tools run, then exactly one result or one error. Failures before the model starts (origin, rate limit, validation, budget) are ordinary JSON errors with a status code, not a stream. Destructive requests ("delete all tasks") are refused before any model runs, at no cost. Asks are limited to 20 per ten minutes per address, and the assistant has a hard daily token budget. A browser request must come from this site (the Origin header is checked); a request with no Origin, such as curl, is accepted.

Request body

The request, and optionally the option the person picked after a question (followUp). application/json, required · AskRequest

FieldTypeNotes
promptstring, 2 to 500 charactersrequired
followUpobjectoptionalThe option the person picked after the assistant asked a question.
followUp.questionstring, 1 to 200 charactersrequired
followUp.labelstring, 1 to 120 charactersrequired
followUp.valuestring, 1 to 60 charactersrequired

Try it

curl -sSN -X POST "$BASE/api/ask" \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"Move all of Priya\u0027s tasks due this week to next sprint"}'

Responses

200 OK

A stream of events, one JSON object per line (application/x-ndjson). Each line matches the AskEvent schema.

Body application/x-ndjson · AskEvent

{"type":"activity","tool":"search_tasks","label":"Searching tasks"}
{"type":"activity","tool":"propose_changes","label":"Checking changes against the policy"}
{"type":"result","askId":"9d1c7a4e-2c61-4f52-8c0b-5b1d0a6f7e21","result":{"kind":"proposal","changeSet":{"id":"6c1a0c1e-5f0e-4d9e-9a57-2f1d3c6d9b10","prompt":"Move all of Priya's tasks due this week to next sprint","summary":"Move 7 of Priya's tasks due this week to Sprint 43.","status":"proposed","proposedBy":"anthropic:claude-haiku-5-5","appliedBy":null,"createdAt":"2026-10-23T08:14:09.512Z","appliedAt":null,"undoneAt":null,"cancelledAt":null,"ops":[{"id":"3f0d2b2a-8d5c-4b7e-9d3e-9d0f7a1c2e44","position":0,"taskId":"WEB-21","taskTitle":"Analytics events audit","field":"sprint","before":"sprint-42","after":"sprint-43","beforeLabel":"Sprint 42","afterLabel":"Sprint 43","applied":false,"included":true}],"rejected":[],"trimmed":0,"tokensIn":2310,"tokensOut":180,"costUsd":0.000495,"tools":[{"position":0,"name":"search_tasks","ok":true,"summary":{"total":7,"shown":7,"ids":["WEB-21"]},"error":null,"durationMs":41}]}}}
400 Bad Request

The body is not JSON, is too large (over 4 KB), or fails validation. Codes: invalid_request.

Body application/json · Error

{
  "error": {
    "code": "invalid_request",
    "message": "The body is not JSON, is too large (over 4 KB), or fails validation.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
403 Forbidden

The request came from another site. Browsers send an Origin header; it must be this site. Codes: forbidden_origin.

Body application/json · Error

{
  "error": {
    "code": "forbidden_origin",
    "message": "The request came from another site. Browsers send an Origin header; it must be this site.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
429 Too Many Requests

Too many requests from this address. Codes: rate_limited.

  • Retry-After Seconds to wait before trying again.

Body application/json · Error

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Try again in 42 seconds.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11",
    "retryAfterSeconds": 42
  }
}
500 Internal Server Error

Something unexpected failed. Nothing was changed. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "Something unexpected failed. Nothing was changed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
503 Service Unavailable

No model is configured, or the daily token budget is spent (it resets at 00:00 UTC). Codes: unavailable, budget_exhausted.

  • Retry-After With budget_exhausted: seconds until the budget resets.

Body application/json · Error

{
  "error": {
    "code": "budget_exhausted",
    "message": "The assistant has used its model budget for today. It resets at 00:00 UTC.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11",
    "resetsAt": "2026-10-24T00:00:00.000Z"
  }
}

Changes

The proposals the assistant made, and the only calls that write to tasks.

GET /api/changes

List change sets

The change log, newest first, with the number of change sets in each status (counted over all of them, whatever the filter).

Parameters

NameInTypeNotes
statusqueryone of proposed, applied, undone, cancelledoptionalOnly change sets in this status.
limitqueryinteger, 1 to 100optionalAt most 100. Default 50.

Try it

curl -sS "$BASE/api/changes"

Responses

200 OK

The change sets, with counts.

Body application/json · ChangesResponse

400 Bad Request

status or limit is not valid. Codes: invalid_request.

Body application/json · Error

{
  "error": {
    "code": "invalid_request",
    "message": "`status` or `limit` is not valid.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
500 Internal Server Error

Something unexpected failed. Nothing was changed. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "Something unexpected failed. Nothing was changed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}

GET /api/changes/{id}

Read one change set

One change set with its diff (ops), what the policy refused (rejected), the assistant steps that led to it (tools), its token usage and cost.

Parameters

NameInTypeNotes
idpathstring (uuid)requiredThe change set id (a UUID).

Try it

curl -sS "$BASE/api/changes/$ID"

Responses

200 OK

The change set.

Body application/json · ChangeResponse

{
  "changeSet": {
    "id": "6c1a0c1e-5f0e-4d9e-9a57-2f1d3c6d9b10",
    "prompt": "Move all of Priya's tasks due this week to next sprint",
    "summary": "Move 7 of Priya's tasks due this week to Sprint 43.",
    "status": "proposed",
    "proposedBy": "anthropic:claude-haiku-5-5",
    "appliedBy": null,
    "createdAt": "2026-10-23T08:14:09.512Z",
    "appliedAt": null,
    "undoneAt": null,
    "cancelledAt": null,
    "ops": [
      {
        "id": "3f0d2b2a-8d5c-4b7e-9d3e-9d0f7a1c2e44",
        "position": 0,
        "taskId": "WEB-21",
        "taskTitle": "Analytics events audit",
        "field": "sprint",
        "before": "sprint-42",
        "after": "sprint-43",
        "beforeLabel": "Sprint 42",
        "afterLabel": "Sprint 43",
        "applied": false,
        "included": true
      }
    ],
    "rejected": [],
    "trimmed": 0,
    "tokensIn": 2310,
    "tokensOut": 180,
    "costUsd": 0.000495,
    "tools": [
      {
        "position": 0,
        "name": "search_tasks",
        "ok": true,
        "summary": {
          "total": 7,
          "shown": 7,
          "ids": [
            "WEB-21"
          ]
        },
        "error": null,
        "durationMs": 41
      }
    ]
  }
}
400 Bad Request

The id is not a UUID. Codes: invalid_request.

Body application/json · Error

{
  "error": {
    "code": "invalid_request",
    "message": "The id is not a UUID.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
404 Not Found

There is no such change set. Codes: not_found.

Body application/json · Error

{
  "error": {
    "code": "not_found",
    "message": "There is no such change set.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
500 Internal Server Error

Something unexpected failed. Nothing was changed. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "Something unexpected failed. Nothing was changed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}

POST /api/changes/{id}/apply

Apply a proposal

Writes every change in the proposal in one atomic step. Each change is re-checked against what its task holds now: if any task has moved since the proposal, nothing is written and the answer is 409 stale with the rows that changed. exclude leaves out the changes a person unticked; they are kept on record as not included and never touch a task. Applying twice is refused. A browser request must come from this site (the Origin header is checked); a request with no Origin, such as curl, is accepted. Writes are limited to 60 per ten minutes per address.

Parameters

NameInTypeNotes
idpathstring (uuid)requiredThe change set id (a UUID).

Request body

Optional. An empty body applies everything. application/json, optional · ApplyRequest

FieldTypeNotes
excludelist of string (uuid), at most 25 itemsoptionalIds of changes to leave out (the unticked rows). They are never written.

Try it

curl -sS -X POST "$BASE/api/changes/$ID/apply" \
  -H 'Content-Type: application/json' \
  -d '{"exclude":["3f0d2b2a-8d5c-4b7e-9d3e-9d0f7a1c2e44"]}'

Responses

200 OK

Applied. touched lists the tasks that were written.

Body application/json · TransitionResponse

400 Bad Request

The id is not a UUID, the body is not valid, or every change was left out. Codes: invalid_request.

Body application/json · Error

{
  "error": {
    "code": "invalid_request",
    "message": "The id is not a UUID, the body is not valid, or every change was left out.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
403 Forbidden

The request came from another site. Browsers send an Origin header; it must be this site. Codes: forbidden_origin.

Body application/json · Error

{
  "error": {
    "code": "forbidden_origin",
    "message": "The request came from another site. Browsers send an Origin header; it must be this site.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
404 Not Found

There is no such change set. Codes: not_found.

Body application/json · Error

{
  "error": {
    "code": "not_found",
    "message": "There is no such change set.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
409 Conflict

not_proposed: it is already applied, undone or cancelled (the body says which). stale: a task changed since the proposal; nothing was written. Codes: not_proposed, stale.

Body application/json · Error

{
  "error": {
    "code": "stale",
    "message": "The board changed since this was proposed. Nothing was applied.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11",
    "conflicts": [
      {
        "opId": "3f0d2b2a-8d5c-4b7e-9d3e-9d0f7a1c2e44",
        "taskId": "WEB-21",
        "taskTitle": "Analytics events audit",
        "field": "sprint",
        "expected": "sprint-42",
        "actual": "sprint-43"
      }
    ]
  }
}
429 Too Many Requests

Too many requests from this address. Codes: rate_limited.

  • Retry-After Seconds to wait before trying again.

Body application/json · Error

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Try again in 42 seconds.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11",
    "retryAfterSeconds": 42
  }
}
500 Internal Server Error

Something unexpected failed. Nothing was changed. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "Something unexpected failed. Nothing was changed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}

POST /api/changes/{id}/undo

Undo an applied change set

Puts every applied change back with its inverse, atomically and re-checked the same way: if a task was edited after the apply, nothing is undone and the answer is 409 stale. Undoing twice is refused. There is no time limit in the demo. A browser request must come from this site (the Origin header is checked); a request with no Origin, such as curl, is accepted. Writes are limited to 60 per ten minutes per address.

Parameters

NameInTypeNotes
idpathstring (uuid)requiredThe change set id (a UUID).

Try it

curl -sS -X POST "$BASE/api/changes/$ID/undo"

Responses

200 OK

Undone. touched lists the tasks that were written.

Body application/json · TransitionResponse

400 Bad Request

The id is not a UUID. Codes: invalid_request.

Body application/json · Error

{
  "error": {
    "code": "invalid_request",
    "message": "The id is not a UUID.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
403 Forbidden

The request came from another site. Browsers send an Origin header; it must be this site. Codes: forbidden_origin.

Body application/json · Error

{
  "error": {
    "code": "forbidden_origin",
    "message": "The request came from another site. Browsers send an Origin header; it must be this site.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
404 Not Found

There is no such change set. Codes: not_found.

Body application/json · Error

{
  "error": {
    "code": "not_found",
    "message": "There is no such change set.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
409 Conflict

not_applied: it was never applied, or is already undone (the body says which). stale: a task was changed after the apply; nothing was undone. Codes: not_applied, stale.

Body application/json · Error

{
  "error": {
    "code": "not_applied",
    "message": "`not_applied`: it was never applied, or is already undone (the body says which). `stale`: a task was changed after the apply; nothing was undone.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
429 Too Many Requests

Too many requests from this address. Codes: rate_limited.

  • Retry-After Seconds to wait before trying again.

Body application/json · Error

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Try again in 42 seconds.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11",
    "retryAfterSeconds": 42
  }
}
500 Internal Server Error

Something unexpected failed. Nothing was changed. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "Something unexpected failed. Nothing was changed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}

POST /api/changes/{id}/cancel

Cancel a proposal

Drops a proposal that has not been applied. Nothing is written to tasks. A browser request must come from this site (the Origin header is checked); a request with no Origin, such as curl, is accepted. Writes are limited to 60 per ten minutes per address.

Parameters

NameInTypeNotes
idpathstring (uuid)requiredThe change set id (a UUID).

Try it

curl -sS -X POST "$BASE/api/changes/$ID/cancel"

Responses

200 OK

Cancelled.

Body application/json · ChangeResponse

400 Bad Request

The id is not a UUID. Codes: invalid_request.

Body application/json · Error

{
  "error": {
    "code": "invalid_request",
    "message": "The id is not a UUID.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
403 Forbidden

The request came from another site. Browsers send an Origin header; it must be this site. Codes: forbidden_origin.

Body application/json · Error

{
  "error": {
    "code": "forbidden_origin",
    "message": "The request came from another site. Browsers send an Origin header; it must be this site.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
404 Not Found

There is no such change set. Codes: not_found.

Body application/json · Error

{
  "error": {
    "code": "not_found",
    "message": "There is no such change set.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
409 Conflict

It is already applied, undone or cancelled (the body says which). Codes: not_proposed.

Body application/json · Error

{
  "error": {
    "code": "not_proposed",
    "message": "It is already applied, undone or cancelled (the body says which).",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
429 Too Many Requests

Too many requests from this address. Codes: rate_limited.

  • Retry-After Seconds to wait before trying again.

Body application/json · Error

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Try again in 42 seconds.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11",
    "retryAfterSeconds": 42
  }
}
500 Internal Server Error

Something unexpected failed. Nothing was changed. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "Something unexpected failed. Nothing was changed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}

Workspace

Read the board, the team and the sprints.

GET /api/board

Read the board

Projects, sprints, people with their workload, and every task. The board page polls this every 20 seconds.

Try it

curl -sS "$BASE/api/board"

Responses

200 OK

The whole workspace.

Body application/json · Board

500 Internal Server Error

Something unexpected failed. Nothing was changed. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "Something unexpected failed. Nothing was changed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}

GET /api/people

List people

The team, active people first. workload is open tasks in the active sprint, against capacity. Someone with active: false has left the team and cannot be assigned work.

Try it

curl -sS "$BASE/api/people"

Responses

200 OK

The team.

Body application/json · PeopleResponse

500 Internal Server Error

Something unexpected failed. Nothing was changed. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "Something unexpected failed. Nothing was changed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}

GET /api/sprints

List sprints

Every sprint, oldest first. Only sprints that are not closed can take tasks.

Try it

curl -sS "$BASE/api/sprints"

Responses

200 OK

The sprints.

Body application/json · SprintsResponse

500 Internal Server Error

Something unexpected failed. Nothing was changed. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "Something unexpected failed. Nothing was changed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}

Platform

Health, the nightly reseed and this document.

GET /api/cron/reseed

Reseed the demo workspace

Puts the shared demo back to its fixtures. Vercel Cron calls it nightly with Authorization: Bearer <CRON_SECRET>; it is not for visitors. The token budget, rate limits and eval history are not touched. Without a configured secret the route is closed (503), never open. At most 6 calls an hour.

Auth bearer secret (Authorization: Bearer <CRON_SECRET>)

Try it

curl -sS "$BASE/api/cron/reseed" \
  -H "Authorization: Bearer $CRON_SECRET"

Responses

200 OK

The workspace was rebuilt.

Body application/json · ReseedResponse

401 Unauthorized

No bearer secret, or the wrong one. Codes: unauthorized.

  • WWW-Authenticate Always Bearer realm="cron".

Body application/json · Error

{
  "error": {
    "code": "unauthorized",
    "message": "No bearer secret, or the wrong one.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
429 Too Many Requests

Too many requests from this address. Codes: rate_limited.

  • Retry-After Seconds to wait before trying again.

Body application/json · Error

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Try again in 42 seconds.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11",
    "retryAfterSeconds": 42
  }
}
500 Internal Server Error

The reseed failed twice. The previous workspace is untouched. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "The reseed failed twice. The previous workspace is untouched.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
503 Service Unavailable

The deployment has no CRON_SECRET, so the route is closed. Codes: not_configured.

Body application/json · Error

{
  "error": {
    "code": "not_configured",
    "message": "The deployment has no CRON_SECRET, so the route is closed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}

POST /api/cron/reseed

Reseed the demo workspace (POST)

Puts the shared demo back to its fixtures. Vercel Cron calls it nightly with Authorization: Bearer <CRON_SECRET>; it is not for visitors. The token budget, rate limits and eval history are not touched. Without a configured secret the route is closed (503), never open. At most 6 calls an hour.

Auth bearer secret (Authorization: Bearer <CRON_SECRET>)

Try it

curl -sS -X POST "$BASE/api/cron/reseed" \
  -H "Authorization: Bearer $CRON_SECRET"

Responses

200 OK

The workspace was rebuilt.

Body application/json · ReseedResponse

401 Unauthorized

No bearer secret, or the wrong one. Codes: unauthorized.

  • WWW-Authenticate Always Bearer realm="cron".

Body application/json · Error

{
  "error": {
    "code": "unauthorized",
    "message": "No bearer secret, or the wrong one.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
429 Too Many Requests

Too many requests from this address. Codes: rate_limited.

  • Retry-After Seconds to wait before trying again.

Body application/json · Error

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Try again in 42 seconds.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11",
    "retryAfterSeconds": 42
  }
}
500 Internal Server Error

The reseed failed twice. The previous workspace is untouched. Codes: internal.

Body application/json · Error

{
  "error": {
    "code": "internal",
    "message": "The reseed failed twice. The previous workspace is untouched.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}
503 Service Unavailable

The deployment has no CRON_SECRET, so the route is closed. Codes: not_configured.

Body application/json · Error

{
  "error": {
    "code": "not_configured",
    "message": "The deployment has no CRON_SECRET, so the route is closed.",
    "requestId": "0b6b1f5e-6f43-4d1e-9a64-0e3a5b9d2c11"
  }
}

GET /api/health

Liveness probe

Answers without touching the database, so a probe never costs a query.

Try it

curl -sS "$BASE/api/health"

Responses

200 OK

The server is up.

Body application/json · Health

{
  "ok": true
}

GET /api/openapi.json

This document

The OpenAPI 3.1 document, generated from the same Zod schemas the routes validate with. Readable from any origin.

Try it

curl -sS "$BASE/api/openapi.json"

Responses

200 OK

The document.

Body application/json · object

GET /api/docs

This reference, as a page

The same document rendered on the server as plain HTML. It runs no script.

Try it

curl -sS "$BASE/api/docs"

Responses

200 OK

An HTML page.

Body text/html · string

Schemas

The shapes above, as they appear in the document under components.schemas. Response objects are closed: a field not listed here is a bug.

Person

FieldTypeNotes
idstringrequiredA short readable id such as "priya".
namestringrequired
initialsstringrequired
rolestringrequired
activebooleanrequiredFalse for someone who has left the team and cannot take work.
capacityintegerrequiredOpen tasks in the active sprint that make a full plate.
workloadintegerrequiredOpen tasks (todo, doing, waiting) in the active sprint.

Sprint

FieldTypeNotes
idstringrequiredFor example "sprint-42".
namestringrequired
startsOnstringrequired
endsOnstringrequired
stateone of closed, active, plannedrequired

Project

FieldTypeNotes
idstringrequired
keystringrequiredThe prefix of task ids, for example "WEB".
namestringrequired

Task

FieldTypeNotes
idstringrequiredFor example "WEB-21".
projectIdstringrequired
titlestringrequired
statusone of todo, doing, waiting, done, archivedrequired
priorityone of low, medium, high, urgentrequired
dueDatestring or nullrequired
assigneeIdstring or nullrequired
sprintIdstring or nullrequiredNull means the backlog.
labelslist of stringrequired
commentsintegerrequired
createdAtstring (date-time)required

Board

FieldTypeNotes
todaystringrequiredThe board's own "today" (UTC).
projectslist of Projectrequired
sprintslist of Sprintrequired
peoplelist of Personrequired
taskslist of Taskrequired

Rejection

FieldTypeNotes
taskIdstringrequired
fieldstringrequired
codeone of unknown_task, field_not_allowed, invalid_value, no_change, duplicate, task_archived, archive_requires_done, due_out_of_range, assignee_unavailable, sprint_unavailable, cap_exceeded, no_deleterequired
messagestringrequiredA plain sentence saying why the policy refused this change.

ChangeOp

FieldTypeNotes
idstring (uuid)required
positionintegerrequired
taskIdstringrequired
taskTitlestringrequired
fieldone of status, priority, due, assignee, sprintrequired
beforestring or nullrequiredWhat the task held when the proposal was made.
afterstring or nullrequiredWhat the proposal would set.
beforeLabelstring or nullrequiredA person or sprint name for an id; otherwise the value.
afterLabelstring or nullrequired
appliedbooleanrequiredTrue while after is what the task holds.
includedbooleanrequiredFalse when the person unticked the change before applying.

ToolCall

FieldTypeNotes
positionintegerrequired
namestringrequired
okbooleanrequired
summaryanyrequiredIds, counts and statuses, never whole result bodies.
errorstring or nullrequired
durationMsintegerrequired

ChangeSet

FieldTypeNotes
idstring (uuid)required
promptstringrequiredThe request as stored, with contact details removed.
summarystringrequired
statusone of proposed, applied, undone, cancelledrequired
proposedBystringrequiredprovider:model that proposed it.
appliedBystring or nullrequired
createdAtstring (date-time)required
appliedAtstring (date-time) or nullrequired
undoneAtstring (date-time) or nullrequired
cancelledAtstring (date-time) or nullrequired
opslist of ChangeOprequired
rejectedlist of RejectionrequiredChanges the policy refused, with reasons.
trimmedintegerrequiredValid changes cut for being over the 25-change cap.
tokensInintegerrequired
tokensOutintegerrequired
costUsdnumberrequired
toolslist of ToolCallrequired

Conflict

FieldTypeNotes
opIdstring (uuid)required
taskIdstringrequired
taskTitlestringrequired
fieldone of status, priority, due, assignee, sprintrequired
expectedstring or nullrequiredWhat the proposal (or undo) expected the task to hold.
actualstring or nullrequiredWhat the task holds now.

Clarification

FieldTypeNotes
questionstringrequired
optionslist of objectrequired
options[].labelstringrequired
options[].valuestringrequired

AskResult

One of these shapes, told apart by the field in each heading.

kind = "proposal"

FieldTypeNotes
kind"proposal"required
changeSetChangeSetrequired

kind = "clarification"

FieldTypeNotes
kind"clarification"required
clarificationClarificationrequired

kind = "refusal"

FieldTypeNotes
kind"refusal"required
messagestringrequired
reasonslist of Rejectionrequired

kind = "answer"

FieldTypeNotes
kind"answer"required
messagestringrequired

AskEvent

One of these shapes, told apart by the field in each heading.

type = "activity"

FieldTypeNotes
type"activity"required
toolstringrequired
labelstringrequiredWhat the assistant is doing, for example "Searching tasks".

type = "result"

FieldTypeNotes
type"result"required
askIdstring (uuid)required
resultAskResultrequired

type = "error"

FieldTypeNotes
type"error"required
codeone of unavailable, internalrequired
messagestringrequired

ChangeCounts

FieldTypeNotes
proposedintegerrequired
appliedintegerrequired
undoneintegerrequired
cancelledintegerrequired

Error

FieldTypeNotes
errorobjectrequired
error.codeone of invalid_request, forbidden_origin, unauthorized, not_found, not_proposed, not_applied, stale, rate_limited, budget_exhausted, unavailable, not_configured, internalrequiredMachine-readable. Branch on this, never on the message.
error.messagestringrequiredSafe to show to a person.
error.requestIdstringrequiredAlso sent as the X-Request-Id header; quote it in a report.
error.statusone of proposed, applied, undone, cancelledoptionalWith not_proposed and not_applied: the status the change set is in.
error.conflictslist of ConflictoptionalWith stale: the rows that no longer match. Nothing was written.
error.retryAfterSecondsintegeroptionalWith rate_limited.
error.resetsAtstring (date-time)optionalWith budget_exhausted: the next 00:00 UTC.

Health

FieldTypeNotes
oktruerequired

PeopleResponse

FieldTypeNotes
peoplelist of Personrequired

SprintsResponse

FieldTypeNotes
sprintslist of Sprintrequired

ChangesResponse

FieldTypeNotes
changeslist of ChangeSetrequired
countsChangeCountsrequiredChange sets per status, whatever the filter.

ChangeResponse

FieldTypeNotes
changeSetChangeSetrequired

TransitionResponse

FieldTypeNotes
changeSetChangeSetrequired
touchedlist of stringrequiredIds of the tasks that were written.

ReseedResponse

FieldTypeNotes
oktruerequired
seededobjectrequired
seeded.projectsintegerrequired
seeded.sprintsintegerrequired
seeded.peopleintegerrequired
seeded.tasksintegerrequired
seeded.commentsintegerrequired
msintegerrequired

AskRequest

FieldTypeNotes
promptstring, 2 to 500 charactersrequired
followUpobjectoptionalThe option the person picked after the assistant asked a question.
followUp.questionstring, 1 to 200 charactersrequired
followUp.labelstring, 1 to 120 charactersrequired
followUp.valuestring, 1 to 60 charactersrequired

ApplyRequest

FieldTypeNotes
excludelist of string (uuid), at most 25 itemsoptionalIds of changes to leave out (the unticked rows). They are never written.