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
$BASEfor its address and$IDfor a change set id.
Endpoints
| Method | Path | What it does |
|---|---|---|
| POST | /api/ask | Ask the assistant for a change |
| GET | /api/changes | List change sets |
| GET | /api/changes/{id} | Read one change set |
| POST | /api/changes/{id}/apply | Apply a proposal |
| POST | /api/changes/{id}/undo | Undo an applied change set |
| POST | /api/changes/{id}/cancel | Cancel a proposal |
| GET | /api/board | Read the board |
| GET | /api/people | List people |
| GET | /api/sprints | List sprints |
| GET | /api/cron/reseed | Reseed the demo workspace |
| POST | /api/cron/reseed | Reseed the demo workspace (POST) |
| GET | /api/health | Liveness probe |
| GET | /api/openapi.json | This document |
| GET | /api/docs | This 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": [
"..."
]
}
}
| Code | Status | Meaning |
|---|---|---|
invalid_request | 400 | The request is not valid: bad JSON, too large, a field out of range, or a malformed id. |
forbidden_origin | 403 | A browser sent a write from another site. Calls without an Origin header are accepted. |
unauthorized | 401 | The cron route needs its bearer secret. |
not_found | 404 | No such change set, or no such endpoint. |
not_proposed | 409 | The change set is no longer a proposal, so it cannot be applied or cancelled. status says what it is. |
not_applied | 409 | The change set is not in the applied state, so it cannot be undone. status says what it is. |
stale | 409 | A task changed since the proposal (or the apply). Nothing was written; conflicts lists the rows. |
rate_limited | 429 | Too many requests from this address. Wait retryAfterSeconds. |
budget_exhausted | 503 | The assistant spent its daily model budget. resetsAt is the next 00:00 UTC. |
unavailable | 503 | No model is configured on this deployment. |
not_configured | 503 | The route is closed because the deployment has no secret for it. |
internal | 500 | Something 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).
| Field | Type | Notes | |
|---|---|---|---|
prompt | string, 2 to 500 characters | required | |
followUp | object | optional | The option the person picked after the assistant asked a question. |
followUp.question | string, 1 to 200 characters | required | |
followUp.label | string, 1 to 120 characters | required | |
followUp.value | string, 1 to 60 characters | required |
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.
{"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.
{
"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.
{
"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-AfterSeconds to wait before trying again.
{
"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.
{
"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-AfterWithbudget_exhausted: seconds until the budget resets.
{
"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
| Name | In | Type | Notes | |
|---|---|---|---|---|
status | query | one of proposed, applied, undone, cancelled | optional | Only change sets in this status. |
limit | query | integer, 1 to 100 | optional | At most 100. Default 50. |
Try it
curl -sS "$BASE/api/changes"
Responses
200 OK
The change sets, with counts.
400 Bad Request
status or limit is not valid. Codes: invalid_request.
{
"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.
{
"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
| Name | In | Type | Notes | |
|---|---|---|---|---|
id | path | string (uuid) | required | The change set id (a UUID). |
Try it
curl -sS "$BASE/api/changes/$ID"
Responses
200 OK
The change set.
{
"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.
{
"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.
{
"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.
{
"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
| Name | In | Type | Notes | |
|---|---|---|---|---|
id | path | string (uuid) | required | The change set id (a UUID). |
Request body
Optional. An empty body applies everything.
| Field | Type | Notes | |
|---|---|---|---|
exclude | list of string (uuid), at most 25 items | optional | Ids 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.
400 Bad Request
The id is not a UUID, the body is not valid, or every change was left out. Codes: invalid_request.
{
"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.
{
"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.
{
"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.
{
"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-AfterSeconds to wait before trying again.
{
"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.
{
"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
| Name | In | Type | Notes | |
|---|---|---|---|---|
id | path | string (uuid) | required | The 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.
400 Bad Request
The id is not a UUID. Codes: invalid_request.
{
"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.
{
"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.
{
"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.
{
"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-AfterSeconds to wait before trying again.
{
"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.
{
"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
| Name | In | Type | Notes | |
|---|---|---|---|---|
id | path | string (uuid) | required | The change set id (a UUID). |
Try it
curl -sS -X POST "$BASE/api/changes/$ID/cancel"
Responses
200 OK
Cancelled.
400 Bad Request
The id is not a UUID. Codes: invalid_request.
{
"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.
{
"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.
{
"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.
{
"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-AfterSeconds to wait before trying again.
{
"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.
{
"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.
500 Internal Server Error
Something unexpected failed. Nothing was changed. Codes: internal.
{
"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.
500 Internal Server Error
Something unexpected failed. Nothing was changed. Codes: internal.
{
"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.
500 Internal Server Error
Something unexpected failed. Nothing was changed. Codes: internal.
{
"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.
Try it
curl -sS "$BASE/api/cron/reseed" \
-H "Authorization: Bearer $CRON_SECRET"
Responses
200 OK
The workspace was rebuilt.
401 Unauthorized
No bearer secret, or the wrong one. Codes: unauthorized.
WWW-AuthenticateAlwaysBearer realm="cron".
{
"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-AfterSeconds to wait before trying again.
{
"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.
{
"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.
{
"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.
Try it
curl -sS -X POST "$BASE/api/cron/reseed" \
-H "Authorization: Bearer $CRON_SECRET"
Responses
200 OK
The workspace was rebuilt.
401 Unauthorized
No bearer secret, or the wrong one. Codes: unauthorized.
WWW-AuthenticateAlwaysBearer realm="cron".
{
"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-AfterSeconds to wait before trying again.
{
"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.
{
"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.
{
"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.
{
"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.
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.
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
| Field | Type | Notes | |
|---|---|---|---|
id | string | required | A short readable id such as "priya". |
name | string | required | |
initials | string | required | |
role | string | required | |
active | boolean | required | False for someone who has left the team and cannot take work. |
capacity | integer | required | Open tasks in the active sprint that make a full plate. |
workload | integer | required | Open tasks (todo, doing, waiting) in the active sprint. |
Sprint
| Field | Type | Notes | |
|---|---|---|---|
id | string | required | For example "sprint-42". |
name | string | required | |
startsOn | string | required | |
endsOn | string | required | |
state | one of closed, active, planned | required |
Project
| Field | Type | Notes | |
|---|---|---|---|
id | string | required | |
key | string | required | The prefix of task ids, for example "WEB". |
name | string | required |
Task
| Field | Type | Notes | |
|---|---|---|---|
id | string | required | For example "WEB-21". |
projectId | string | required | |
title | string | required | |
status | one of todo, doing, waiting, done, archived | required | |
priority | one of low, medium, high, urgent | required | |
dueDate | string or null | required | |
assigneeId | string or null | required | |
sprintId | string or null | required | Null means the backlog. |
labels | list of string | required | |
comments | integer | required | |
createdAt | string (date-time) | required |
Board
Rejection
| Field | Type | Notes | |
|---|---|---|---|
taskId | string | required | |
field | string | required | |
code | one 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_delete | required | |
message | string | required | A plain sentence saying why the policy refused this change. |
ChangeOp
| Field | Type | Notes | |
|---|---|---|---|
id | string (uuid) | required | |
position | integer | required | |
taskId | string | required | |
taskTitle | string | required | |
field | one of status, priority, due, assignee, sprint | required | |
before | string or null | required | What the task held when the proposal was made. |
after | string or null | required | What the proposal would set. |
beforeLabel | string or null | required | A person or sprint name for an id; otherwise the value. |
afterLabel | string or null | required | |
applied | boolean | required | True while after is what the task holds. |
included | boolean | required | False when the person unticked the change before applying. |
ToolCall
| Field | Type | Notes | |
|---|---|---|---|
position | integer | required | |
name | string | required | |
ok | boolean | required | |
summary | any | required | Ids, counts and statuses, never whole result bodies. |
error | string or null | required | |
durationMs | integer | required |
ChangeSet
| Field | Type | Notes | |
|---|---|---|---|
id | string (uuid) | required | |
prompt | string | required | The request as stored, with contact details removed. |
summary | string | required | |
status | one of proposed, applied, undone, cancelled | required | |
proposedBy | string | required | provider:model that proposed it. |
appliedBy | string or null | required | |
createdAt | string (date-time) | required | |
appliedAt | string (date-time) or null | required | |
undoneAt | string (date-time) or null | required | |
cancelledAt | string (date-time) or null | required | |
ops | list of ChangeOp | required | |
rejected | list of Rejection | required | Changes the policy refused, with reasons. |
trimmed | integer | required | Valid changes cut for being over the 25-change cap. |
tokensIn | integer | required | |
tokensOut | integer | required | |
costUsd | number | required | |
tools | list of ToolCall | required |
Conflict
| Field | Type | Notes | |
|---|---|---|---|
opId | string (uuid) | required | |
taskId | string | required | |
taskTitle | string | required | |
field | one of status, priority, due, assignee, sprint | required | |
expected | string or null | required | What the proposal (or undo) expected the task to hold. |
actual | string or null | required | What the task holds now. |
Clarification
| Field | Type | Notes | |
|---|---|---|---|
question | string | required | |
options | list of object | required | |
options[].label | string | required | |
options[].value | string | required |
AskResult
kind = "proposal"
| Field | Type | Notes | |
|---|---|---|---|
kind | "proposal" | required | |
changeSet | ChangeSet | required |
kind = "clarification"
| Field | Type | Notes | |
|---|---|---|---|
kind | "clarification" | required | |
clarification | Clarification | required |
kind = "refusal"
| Field | Type | Notes | |
|---|---|---|---|
kind | "refusal" | required | |
message | string | required | |
reasons | list of Rejection | required |
kind = "answer"
| Field | Type | Notes | |
|---|---|---|---|
kind | "answer" | required | |
message | string | required |
AskEvent
type = "activity"
| Field | Type | Notes | |
|---|---|---|---|
type | "activity" | required | |
tool | string | required | |
label | string | required | What the assistant is doing, for example "Searching tasks". |
type = "result"
| Field | Type | Notes | |
|---|---|---|---|
type | "result" | required | |
askId | string (uuid) | required | |
result | AskResult | required |
type = "error"
| Field | Type | Notes | |
|---|---|---|---|
type | "error" | required | |
code | one of unavailable, internal | required | |
message | string | required |
ChangeCounts
| Field | Type | Notes | |
|---|---|---|---|
proposed | integer | required | |
applied | integer | required | |
undone | integer | required | |
cancelled | integer | required |
Error
| Field | Type | Notes | |
|---|---|---|---|
error | object | required | |
error.code | one of invalid_request, forbidden_origin, unauthorized, not_found, not_proposed, not_applied, stale, rate_limited, budget_exhausted, unavailable, not_configured, internal | required | Machine-readable. Branch on this, never on the message. |
error.message | string | required | Safe to show to a person. |
error.requestId | string | required | Also sent as the X-Request-Id header; quote it in a report. |
error.status | one of proposed, applied, undone, cancelled | optional | With not_proposed and not_applied: the status the change set is in. |
error.conflicts | list of Conflict | optional | With stale: the rows that no longer match. Nothing was written. |
error.retryAfterSeconds | integer | optional | With rate_limited. |
error.resetsAt | string (date-time) | optional | With budget_exhausted: the next 00:00 UTC. |
Health
| Field | Type | Notes | |
|---|---|---|---|
ok | true | required |
PeopleResponse
| Field | Type | Notes | |
|---|---|---|---|
people | list of Person | required |
SprintsResponse
| Field | Type | Notes | |
|---|---|---|---|
sprints | list of Sprint | required |
ChangesResponse
| Field | Type | Notes | |
|---|---|---|---|
changes | list of ChangeSet | required | |
counts | ChangeCounts | required | Change sets per status, whatever the filter. |
ChangeResponse
| Field | Type | Notes | |
|---|---|---|---|
changeSet | ChangeSet | required |
TransitionResponse
| Field | Type | Notes | |
|---|---|---|---|
changeSet | ChangeSet | required | |
touched | list of string | required | Ids of the tasks that were written. |
ReseedResponse
| Field | Type | Notes | |
|---|---|---|---|
ok | true | required | |
seeded | object | required | |
seeded.projects | integer | required | |
seeded.sprints | integer | required | |
seeded.people | integer | required | |
seeded.tasks | integer | required | |
seeded.comments | integer | required | |
ms | integer | required |
AskRequest
| Field | Type | Notes | |
|---|---|---|---|
prompt | string, 2 to 500 characters | required | |
followUp | object | optional | The option the person picked after the assistant asked a question. |
followUp.question | string, 1 to 200 characters | required | |
followUp.label | string, 1 to 120 characters | required | |
followUp.value | string, 1 to 60 characters | required |
ApplyRequest
| Field | Type | Notes | |
|---|---|---|---|
exclude | list of string (uuid), at most 25 items | optional | Ids of changes to leave out (the unticked rows). They are never written. |