# Onn Tasks: agent API

A small kanban tool. Your API key belongs to one board (one project): every request is about that board.
The key is that of one member of the board: an agent, or a person who works through an assistant on their own machine. Either way you act as that member: "you" and `"me"` below mean them, what you do is recorded under their name, and `GET /me` says who that is.
`GET /board` describes the board: what the project is and what to mind (`description`), and its columns. Read it once before you start.
A board has columns, left to right; a task sits in one of them. Every column has a description that says what it means on this board.
The board also says which column agents pull work from and where a claimed task goes (`pickup` in `GET /board`), usually Ready → Doing.
In some columns someone else is up (`handover` in `GET /board`, such as Review or Blocked): a task can only go there assigned to the person or agent who has to act on it, never to yourself. That is what keeps work from circling back to you, and it holds for a person's key as well.
In some columns work ends (`final` in `GET /board`, such as Done or Cancelled). A task that has been there for some days drops out of sight on the board people look at; it still exists, and this API keeps returning it.
A task can be snoozed until a day (`snoozedUntil`, such as 2026-10-15): it waits for something, a deadline or someone's answer. Until that day people do not see it on the board and `POST /tasks/claim` passes it by; `GET /tasks` still lists it, with the day. Leave a snoozed task alone unless you were asked about it.
Every task and epic has a key such as PAC-12: the board's slug and a number. Use it when you refer to a task elsewhere (a branch, a commit, a merge request).
You pull the tasks that are assigned to you, do the work, and report back by moving the task.
A task can carry files: images (screenshots, mock-ups) and anything else that is too long or too much for the description (a transcript, a log, a PDF). They are the `attachments` in every answer about a single task, each with its `name`, `contentType` and a `url` you can fetch without a key; when the brief points at one, fetch and read it. Add one with `POST /tasks/{id}/attachments`, remove one with `DELETE /tasks/{id}/attachments/{attachmentId}`.
Every task keeps a history that is not rewritten: who created it, every move and hand-over, and the notes people and agents left. The one exception: a person may correct the text of their own note in the app; such a note carries `editedAt`. Read it before you start, and leave a note when you stop, so whoever gets the task next knows where it stands.
The board only hands out the work; everything else (code, merge requests, deploys) happens in your own tools.

The same API as an OpenAPI document: `GET /openapi.json`; for people there is a page at `/docs`.

All paths below are relative to the URL of this guide. Every request except this guide needs
`Authorization: Bearer <your key>`. Bodies and answers are JSON.

## The loop

1. `POST /tasks/claim` takes your top task in the pickup column, moves it to the column that follows and returns it.
   `{ "task": null }` means there is nothing for you to do.
2. Do the work that `title` and `description` ask for. The task's `history` tells what happened before it reached you.
3. `PATCH /tasks/{id}` with `{ "status": "<column>", "note": "what you did, with links" }` to move it on. A screenshot of the result helps the reviewer: attach it first.
   `GET /board` lists this board's columns; pick the one that says where the work now stands.

Need something from someone, or a review? Move the task to that column and assign it to them:
`PATCH /tasks/{id}` with `{ "status": "Blocked", "assignee": "<a person>", "note": "I need ..." }`.
A move into a handover column that leaves the task with you (or with nobody) is refused with 400.

## Endpoints

| Request | What it does |
|---|---|
| `GET /me` | Who this key belongs to (`kind` says whether that is an agent or a person), and on which board. |
| `GET /board` | The board: its description (the project, and what to mind), its slug, its columns (left to right, each with its meaning, whether someone else is up there and whether work ends there), where agents pick up work, and its epics, each with its `description`. |
| `GET /tasks` | Your tasks, in board order. `?status=` and `?epic=` filter (comma-separated); `?assignee=all`, `none`, or a member looks beyond your own. |
| `POST /tasks/claim` | Your next task from the pickup column, moved to the one that follows. |
| `GET /tasks/{id}` | One task with its history. Here and in PATCH, `{id}` may be the key (PAC-12) or the id. |
| `PATCH /tasks/{id}` | Change any of `status`, `title`, `description`, `epic`, `assignee`, `snoozedUntil`, and/or leave a `note`. Every change is added to the history. |
| `POST /tasks` | New task: `title` (required), `description`, `epic`, `assignee`, `snoozedUntil`, `status` (default: the first column). |
| `POST /tasks/{id}/attachments?name=<file name>` | Attach a file: send the bytes as the body with their `Content-Type`, up to 10 MB. Any kind of file; give it its name. |
| `DELETE /tasks/{id}/attachments/{attachmentId}` | Remove an attachment. |
| `DELETE /tasks/{id}` | Remove a task you created yourself, with its history and files; a task created by someone else is refused with 403. Work that is simply over goes to a final column instead. |
| `POST /epics` | New epic: `name` (required, new on this board) and `description` (Markdown, what it is about). It gets the next key, at the end of the list. |
| `GET /epics/{id}` | One epic (by name, key or id) with its description and its history, notes included. |
| `PATCH /epics/{id}` | Change `name` or `description` of an epic, and/or leave a `note` on it: where the whole stands, what was decided. Every change is added to its history. |
| `GET /members` | The people and agents a task can be assigned to, each with a `description` of their role when one was given. |

## Fields

- `status`: any column of this board, by name or id; a task may move in any direction. Answers give the name. A handover column needs an `assignee` other than you.
- `description` and `note` are Markdown (lists, **bold**, *italics*, links, code); people see them rendered.
- `note`: added to the task's history under your name. Use it to report what you did; leave `description` alone, it holds the brief. A note alone is fine: `{ "note": "..." }`.
- `assignee`: a member, by name or id, `"me"`, or `null` for nobody. Setting it hands the task over; `GET /members` tells who does what.
- `snoozedUntil`: a day as `YYYY-MM-DD`, or `null` to end the snooze. The task is out of sight until that day (the day itself counts as back; days are taken in Europe/Amsterdam). Answers give the day while it lies ahead, otherwise `null`. Say in a `note` what the task is waiting for.
- `epic`: an existing epic of this board, by name, key or id, or `null`. Epics group tasks; `GET /board` lists them, `POST /epics` adds one, and an epic has a description, notes and a history of its own (`GET /epics/{id}`, `PATCH /epics/{id}`) like a task.

For example: `curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: image/png" --data-binary @after.png "$API/tasks/PAC-12/attachments?name=after.png"`
or, for a long report: `curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: text/markdown" --data-binary @report.md "$API/tasks/PAC-12/attachments?name=report.md"`

A task looks like this. `GET /tasks` lists tasks without `attachments` and `history`; every answer about a single task includes them, oldest first.
History entries have a `type`: `created`, `status`, `assignee`, `epic` (each with `from` and `to`), `edited` (with `fields`), `snoozed` (with `until`, a day or `null`), `attached` or `detached` (with `name`), or `note` (with `text`).

```json
{
  "id": "k3J9…",
  "key": "PAC-12",
  "title": "Add rate limiting to the export endpoint",
  "description": "",
  "epic": "Platform",
  "status": "Doing",
  "assignee": { "id": "9fQ2…", "name": "Claude", "kind": "agent" },
  "snoozedUntil": null,
  "createdAt": "2026-09-30T09:12:44.000Z",
  "updatedAt": "2026-09-30T09:30:02.000Z",
  "attachments": [
    { "id": "a1…", "name": "after.png", "contentType": "image/png", "size": 48213, "url": "https://firebasestorage.googleapis.com/…", "by": { "name": "Claude", "kind": "agent" }, "at": "2026-09-30T09:29:50.000Z" }
  ],
  "history": [
    { "at": "2026-09-30T09:12:44.000Z", "by": { "name": "David", "kind": "person" }, "type": "created" },
    { "at": "2026-09-30T09:12:44.000Z", "by": { "name": "David", "kind": "person" }, "type": "assignee", "from": null, "to": "Claude" },
    { "at": "2026-09-30T09:30:02.000Z", "by": { "name": "Claude", "kind": "agent" }, "type": "status", "from": "To do", "to": "Doing" },
    { "at": "2026-09-30T09:48:10.000Z", "by": { "name": "Claude", "kind": "agent" }, "type": "note", "text": "MR !42 is open." }
  ]
}
```

## Limits

| What | Limit |
|---|---|
| `title` | 200 characters, one line |
| `description` | 20,000 characters |
| `note` | 20,000 characters per note; a longer report can go in several notes |
| attachment | 10 MB, any kind of file; file name up to 120 characters. People see png, jpeg, gif, webp as pictures; anything else is a download |

Text over a limit is refused with 400 and a message that names the field; nothing is cut off silently.

Errors come back as `{ "error": "..." }` with status 400 (bad input), 401 (key missing, wrong or revoked), 403 (not yours to remove), 404 or 409 (a name that is taken).
