Connecting coding agents
A coding agent — Claude Code, Codex, anything that speaks MCP — works board tasks through a remote MCP server your API serves. The design and the decisions behind it are in agent-integration.md.
Everything between
<and>is yours:<KEY>is your task-key prefix (config.keyPrefix),<owner>/<repo>your repository,<basePath>the prefix you mounted the board on, and<board-name>whatever you call the server when you register it.
- URL:
https://<your-api-host>/api/mcp(local:http://localhost:4000/api/mcp) - Transport: streamable HTTP, stateless (POST only, JSON responses)
- Auth:
Authorization: Bearer <key>(orX-API-Key: <key>) with an API key of roleagent. Admin logins and other keys are rejected, and an agent key cannot call any admin route. - Rate limit: 60 calls per minute per key (HTTP 429 beyond that).
1. Get a key#
An admin mints one per agent (per person × tool) in the admin, or with POST <your admin API>/api-keys { "label": "Claude Code · <person>", "role": "agent" }. The label is the agent's display name on the board. The plaintext key is shown once; revoke it from the same screen when it leaks or is no longer used.
2. Connect#
Claude Code#
claude mcp add --transport http <board-name> https://<your-api-host>/api/mcp \
--header "Authorization: Bearer <key>"Check it with claude mcp list, or /mcp inside a session.
Codex#
~/.codex/config.toml:
[mcp_servers.<board-name>]
url = "https://<your-api-host>/api/mcp"
http_headers = { "Authorization" = "Bearer <key>" }3. Branch convention#
Work each task on <key>-<n>-<slug>, e.g. <key>-42-fix-the-search, branched from origin/next (never from main) and opened as a PR against next. get_task_brief and claim_task return the exact name. The PR sync matches the <KEY>-<n> key in the branch, so the PR shows up on the task by itself; link_pr is for the cases it can't match.
Work ships in batches: see releases.md for the release flow. When your PR's checks are green, call add_to_release — that merges it into next so the reporter can test it. A stacked PR (one branched off another task's branch) is refused with stacked_pr until its parent lands; rebase it on origin/next after the parent is added.
4. Tools#
ref is always a task key (<KEY>-42) or its id. Errors come back as isError text, never as a crash, and every call is recorded (BacklogAgentAction) under the agent's key.
| Tool | What it does |
|---|---|
list_tasks({ status?, epic?, assignee?, label?, q?, limit?, offset?, fields? }) | Filtered task cards; assignee: "me" = your tasks. Pages of up to 100; see Paging below. |
get_task({ ref }) | Full task: subtasks, comments (with authorKind), attachments, PRs, code pointers, epic. |
get_task_brief({ ref }) | Spanish markdown work order: fields, criteria, subtasks, code pointers, attachments, PRs, and how to report. |
list_epics({ archived? }) | Active (or archived) epics with their description and task counts per status. |
get_board() | Task counts per column. |
claim_task({ ref, force? }) | Assigns the task to you and moves it to IN_PROGRESS. Fails if another agent holds it, unless force. |
move_task({ ref, status }) | Moves the task to BACKLOG, READY, IN_PROGRESS, BLOCKED or IN_REVIEW. RELEASED and DONE are refused. |
add_progress({ ref, markdown }) | Posts a progress comment, marked as written by an agent. |
link_pr({ ref, number }) | Links a GitHub PR (looked up among the 100 most recent) to the task. |
create_task({ title, description?, acceptanceCriteria?, type?, priority?, estimate?, epicId?, labels?, codePointers? }) | Files a new task in BACKLOG, marked as created by an agent. |
add_subtask({ ref, title }) | Appends a subtask. |
set_acceptance_criteria({ ref, criteria }) | Replaces the criteria: each item is a string or { text, done? }. |
check_criteria({ ref, indexes, done }) | Ticks (done: true) or unticks criteria by their number in the brief. |
add_labels({ ref, labels }) | Adds labels, keeping the existing ones. |
add_attachment({ ref, name, contentType, base64 }) | Uploads evidence (PNG/JPEG/WebP/PDF, ≤ 2 MB, ≤ 12 per task). Returns { id, url }. |
submit_handoff({ ref, howToTest, notes?, previewUrl?, evidenceIds? }) | The entrega: how to test it, risks, preview URL and the evidence ids. |
ask_human({ ref, question, nudge? }) | Posts the question in the task comments, rings the bell (requester first, then creator, then every admin), flags the task "needs your answer" and moves it to BLOCKED unless it is already in review or done. |
list_releases() | Releases, newest first, with their counts. |
get_release({ ref }) | One release (id, slug, or open) with its tasks, the test URLs and production status. |
add_to_release({ ref, prNumber? }) | Adds your finished task to the open release: merges its PR into next. Agent keys may only add a task assigned to them. |
create_release({ name? }) | automation keys only. Opens a release. |
ship_release({ ref, confirm }) | automation keys only. Merges next into main, tags it and moves every task to RELEASED. |
record_ship({ ref, sha, prNumber }) | automation keys only. Records a release that was published by hand (the next → main PR merged outside the Tablero): marks it SHIPPED at sha and moves the cards linked to PR prNumber to RELEASED. Needs sha in main and the PR's merge commit inside it (sha_not_in_main, pr_not_in_sha). Does not tag or touch next. See releases. |
read_replies({ ref, since? }) | Teammate comments after since (ISO 8601), plus the needsInput flag. A teammate reply clears the flag. |
create_epic({ title, summary?, description?, color?, public? }) | Creates an epic; returns it with its id. |
update_epic({ id, title?, summary?, description?, color?, status?, public?, rank? }) | Edits an epic. |
update_task({ ref, title?, description?, type?, priority?, estimate?, epicId?, technicalNotes?, codePointers?, dueDate?, assigneeId? }) | Edits task fields; null clears the nullable ones (epicId: null detaches). Status, criteria and labels keep their own tools. |
remove_labels({ ref, labels }) / list_labels() | Removes labels from a task / every label in use. |
update_subtask({ id, title?, done? }) / delete_subtask({ id }) | Edits or deletes a subtask (ids come in get_task). |
update_comment({ id, markdown }) / delete_comment({ id }) | Edits or deletes a comment. An agent key only its own; an automation key any. |
delete_attachment({ ref, id }) | Deletes an attachment. An agent key only the ones it uploaded; an automation key any. |
update_handoff({ ref, howToTest?, notes? }) | Rewrites the entrega's text; evidence and submitter stay. |
list_drafts() | Open intake drafts (unfinished INTAKE conversations): who asked, the messages, the attachments and the latest draft proposal, if any. |
approve_draft({ conversationId, title, description, type, priority, estimate, acceptanceCriteria?, labels?, codePointers?, technicalNotes?, attachmentIds?, status? }) | Turns a stuck draft into a task through the board's own approval: linked to the conversation, requested by whoever held the interview, draft APPROVED. status is READY (default, like the button) or BACKLOG; without attachmentIds every conversation attachment is copied. |
archive_draft({ conversationId }) | Archives a draft nobody will finish, like the board's Archivar button. An approved draft is refused; archiving twice is a no-op. |
link_task_conversation({ ref, conversationId }) | Repairs a card made by hand from a draft: links it to the conversation and makes the interviewer its requester. Works on archived drafts; refused if the task is already linked to another conversation. |
release_candidates() | Tasks ready to join the open release. |
search_context({ q? }) | Read-only search of the app vocabulary (the GET /context picker). |
archive_epic({ id }) | automation keys only. Archives an epic. |
delete_task({ ref }) | automation keys only. Deletes a task for good. |
bulk_update_tasks({ refs, patch }) | automation keys only. One patch (status, epic, priority, assignee, labels) over many tasks; an empty patch is refused. |
verify_task({ ref, result, note? }) | automation keys only. Production verification: ok → DONE, failed (needs note) → back to IN_PROGRESS with its developer. |
resolve_move_request({ ref, approve }) | automation keys only. Answers an agent's move request. |
update_release({ id, name?, notes?, status? }) | automation keys only. Edits or closes (status: "CLOSED") a release. |
Entrega. Before moving a task to IN_REVIEW, hand it over: upload one to three screenshots with add_attachment (desktop and 390 px), then call submit_handoff with numbered steps somebody who does not program can follow, the preview URL when the change is visible on next, and a note about anything that can only be checked on production. move_task to IN_REVIEW still works without one, but it answers warning: "sin entrega: usa submit_handoff" and the person who asked for the task has nothing to test. The entrega shows on the task page, in get_task, and as a "Cómo probar" section in get_task_brief.
Statuses, in board order: BACKLOG → READY → IN_PROGRESS → BLOCKED → IN_REVIEW → DONE. BLOCKED means waiting on a person: ask_human puts the task there and remembers the column it came from (blockedFromStatus); the first teammate reply in the task comments sends it back to that column (or IN_PROGRESS). Moving a task out of BLOCKED by hand also forgets the origin. The brief shows it as **Estado:** Bloqueada (desde En progreso).
Acceptance criteria are stored as a markdown task list, and get_task_brief numbers them with their boxes (1. [x] …, 2. [ ] …). Tick one as soon as it is met with check_criteria({ ref, indexes: [2], done: true }); only those boxes change, and indexes out of range are ignored and listed in the result's ignored. set_acceptance_criteria replaces the whole list: a plain string keeps the box of an existing criterion with exactly the same (trimmed) text and starts unchecked otherwise, while { text, done } sets the box explicitly.
Paging list_tasks#
list_tasks (and GET <basePath>/tasks, which takes the same offset, limit and fields query params) answers { items, total, page, offset, limit, nextOffset }. A page is at most 100 cards (limit is clamped). To read everything, repeat the call with offset: nextOffset until nextOffset is null; total is the size of the whole filter. offset counts tasks, not pages, and wins over page.
fields: "ids" is the cheap mode: the whole filter as { id, key, status } rows, with no 100 cap (default and hard cap 2000 per call; nextOffset still says whether there are more). Use it to count or diff, then get_task only what you need.
5. Etiquette#
- Read first:
get_task_briefbefore touching code. - Claim before starting:
claim_task, then work on the branch it returns. - Report progress often:
add_progressat each meaningful step (what changed, what's next). The team reads it on the task page. - Use the brand kits: the admin's primitives live in
<your admin kit>and the storefront's in<your storefront kit>(your brand classes). Never hand-style a button, a chip or an input — extend the kit instead.app_contextanswers "botón", "chip" or "button" with the exact component and its file, andget_task_briefnames the likely ones for the app your task touches. - Hand it over:
add_attachment+submit_handoffbefore review — a change nobody can test is not finished. - Ask when blocked:
ask_humanwith a concrete question, then pollread_replieswith thesinceof your question instead of guessing. - Finish in review: open the PR,
link_prif the branch didn't match,move_tasktoIN_REVIEW, andadd_to_releaseonce the checks are green. - File what you find: out-of-scope bugs or follow-ups go in
create_task, not in the current PR.
6. What agents cannot do#
- Move a task to
RELEASEDorDONE. The release assigns "En producción" when it ships, and the person who asked for the task closes it after checking it on production. - Open, ship or record a release.
create_release,ship_releaseandrecord_shipexist only for anautomationkey, which is a person's own admin-level key. - Delete tasks, archive epics, bulk-edit, verify, answer move requests or edit releases: those tools exist only for an
automationkey. - Edit or delete somebody else's comment or attachment.
- Dispatch agents, use the AI chat, conversations or proposals, or read the notifications, usage or repository routes: not exposed over MCP.
- Reach the admin API with an agent key, or mint/revoke keys.
7. Dispatch: "Enviar a agente"#
Instead of an agent pulling work, the board can push a task to one. From the task page (or a backlog row), Enviar a agente › Claude | Codex calls POST <basePath>/tasks/:id/dispatch with { "agent": "claude" | "codex" }:
- Issue mirror. The first time, the API creates a GitHub issue on
<owner>/<repo>titled<KEY>-<n> · <título>. Its body is the same brief asget_task_briefplus a footer linking back to the task, and it gets the labelstableroand<type>. Later dispatches update the title and body and keep the existing labels. If the mirrored issue was deleted, a new one is created. The task storesgithubIssueNumber/githubIssueUrl. - Trigger.
- Claude: a comment
@claude Trabaja esta tarea siguiendo el brief; …. The workflow.github/workflows/claude.yml(Claude Code Action on a hosted runner) answers it. The run has the the board MCP configured, claims the task, reports progress, opens the PR on<key>-<n>-<slug>and moves the task toIN_REVIEW. - Codex: adds the label
codex. The issue is then assigned toGITHUB_CODEX_ASSIGNEEwhen that env var is set on the API; otherwise the API comments@codex …with the same instruction.
- Claude: a comment
- Trail. The task gets a comment "Enviada a Claude/Codex · issue #n", and the activity feed gets a
backlog.task.dispatchrow.
Response: { taskId, key, agent, issueNumber, issueUrl, created, trigger: "comment" | "assignee" }. Errors:
- 409: the GitHub token can't write issues (see setup step a).
- 503: GitHub isn't configured.
- 502: any other GitHub failure.
Running on subscriptions keeps per-token API cost at zero. The only cost is hosted Actions minutes.