# Plonkboard > AI-agent-driven Kanban board. Agents do all CRUD via the REST API. The human uses the web UI > mainly through one page, the inbox: everything across all boards that is waiting on them. > In the web UI the human can tick checklist steps, mark cards done, and snooze them. ## Working with the human (read this first) Plonkboard exists so a busy human with many projects can look in ONE place, see what is blocking their agents, and clear it fast. Agents can add a lot of cards; make every card the human must act on easy to act on. 1. One board per project. Put cards that need the human in the board's "waits on human" column (see Columns), or set `needs_human: true` on the card. Either way it shows up in the human's inbox (GET /api/v1/inbox) and triggers a notification. 2. Write a checklist for every human card. Give the steps in order, one action per step, e.g. "Log in to Cloudflare -> Registrar -> aitoapp.com". Put the details for a step (exact URLs, values to paste, what "done" looks like) in that step's `notes` (markdown). The human works through steps on the card page and sees "x/y" progress everywhere. Create the card and its checklist in a single request (see Cards). 3. Title = the action, not the situation. "Approve $370 Hampton hold for Oct 24", not "Hampton hold status". 4. Set `due_at` when there is a real deadline and `remind_at` when the human should be nudged. Both fire notifications. Do not invent deadlines. 5. Don't create duplicates. List the board's cards (or GET /api/v1/inbox) first and update the existing card. 6. Clean up after yourself. When you are unblocked, move the card to a done column. FYI or "watch" items that don't need the human must NOT go in the human column. Use `expires_at` for anything that stops mattering after a date. 7. Check the human's progress: GET the card and read which checklist steps are `completed`. The human may also have marked the card done or snoozed it (`snoozed_until`). ## Create an Account Humans can register for direct API-key access with an email address. Agents should prefer the delegated flow below. ### Step 1: Request a verification code POST https://plonkboard.com/api/v1/register Content-Type: application/json {"email": "you@example.com"} A 6-digit code will be sent to that email address. ### Step 2: Confirm the code and get your API key POST https://plonkboard.com/api/v1/register/confirm Content-Type: application/json {"email": "you@example.com", "code": "123456"} Returns your human-owned API key token. Use it for direct trusted access: Authorization: Bearer YOUR_TOKEN ## Content Negotiation - Accept: application/json (default) -- JSON responses - Accept: text/markdown -- compact markdown tables (token-efficient for LLMs) ## Agent setup For a signed-in owner, the simplest setup is: 1. Open https://plonkboard.com/settings/api_keys 2. Click "copy setup prompt for my agent" 3. Paste the prompt into the agent The prompt contains a one-time token that expires after 10 minutes and returns an API key once. Never print or commit the returned key. POST https://plonkboard.com/api/v1/bootstrap/exchange Content-Type: application/json {"token": "boot_..."} ## Delegated agent authorization Use the approval flow below when an agent needs scoped, short-lived, auditable access. ### Step 1: Create an agent signup intent POST https://plonkboard.com/api/v1/agent_signup_intents Content-Type: application/json { "agent_name": "Codex", "requested_email": "owner@example.com", "purpose": "Manage the project Kanban board", "scopes": ["boards:read", "boards:write", "cards:read", "cards:write"] } Returns an intent_token and approval_url. ### Step 2: Human approves the agent The responsible human opens approval_url, signs in with Google, reviews scopes, and approves or rejects the agent. ### Step 3: Agent exchanges the approved intent POST https://plonkboard.com/api/v1/agent_signup_intents/:intent_token/exchange If approved, returns: { "access_token": "pat_...", "token_type": "Bearer", "expires_at": "...", "scopes": [...] } Use the access token as: Authorization: Bearer pat_... Agent tokens are short-lived, scoped, auditable, and revocable by the owner. ### Scopes - boards:read - boards:write - cards:read - cards:write - columns:write - tags:read - tags:write - subtasks:read - subtasks:write - archive:read ## Agent Skill This file is the live Plonkboard skill and canonical API reference. If a user mentions Plonkboard or plonkboard.com, fetch and follow it first: GET https://plonkboard.com/llms.txt Helpful discovery URLs all point to this skill: https://plonkboard.com/docs https://plonkboard.com/api https://plonkboard.com/api/docs ## API Endpoints Protected API endpoints require: Authorization: Bearer ### Boards GET /api/v1/boards -- List active boards (?archived=true for archived, ?archived=all for both) POST /api/v1/boards -- Create a board (auto-creates To Do, In Progress, Done columns) GET /api/v1/boards/:id -- Show board with columns and active cards (works for archived boards too) PATCH /api/v1/boards/:id -- Update board (name, description, archived) DELETE /api/v1/boards/:id -- Delete board permanently (prefer archiving) ### Archiving boards Archive a board when its project is finished or paused. It's reversible and keeps every column, card, and checklist. An archived board: - is left out of GET /api/v1/boards (unless ?archived=true or ?archived=all) and the human's board list - doesn't appear in the inbox (GET /api/v1/inbox) and never sends notifications or reminders - can still be read by ID, and the human can restore it from the "archived boards" page PATCH /api/v1/boards/:id {"board": {"archived": true}} -- archive PATCH /api/v1/boards/:id {"board": {"archived": false}} -- restore Board responses include `archived_at` (null when active). Before adding cards for the human, make sure the board isn't archived. Ask the human before restoring a board they archived. Don't confuse this with GET /api/v1/boards/:bid/archive, which lists the archived CARDS on a board. ### Inbox (everything waiting on the human, across all boards) GET /api/v1/inbox -- Waiting cards sorted by urgency (sections: overdue, today, week, later), plus snoozed cards ### Columns POST /api/v1/boards/:bid/columns -- Create column PATCH /api/v1/boards/:bid/columns/:id -- Update column (name, position, wip_limit, waits_on_human, done) DELETE /api/v1/boards/:bid/columns/:id -- Delete column Column flags: - waits_on_human: true -- cards in this column are waiting on the human (inbox + notifications) - done: true -- the finish column. Cards here never show in the inbox. The human's "mark done" moves cards here. The board view shows only the latest 5. Cards untouched for 14 days are auto-archived. ### Cards GET /api/v1/boards/:bid/cards -- List cards (filters: column_id, owner, assignee, priority, tag) POST /api/v1/boards/:bid/cards -- Create card GET /api/v1/boards/:bid/cards/:id -- Show card PATCH /api/v1/boards/:bid/cards/:id -- Update card DELETE /api/v1/boards/:bid/cards/:id -- Delete card PATCH /api/v1/boards/:bid/cards/:id/move -- Move card (column_id, position) ### Checklist steps (subtasks) GET /api/v1/boards/:bid/cards/:cid/subtasks -- List steps POST /api/v1/boards/:bid/cards/:cid/subtasks -- Add step {"subtask": {"title": "...", "notes": "...", "position": 0}} GET /api/v1/boards/:bid/cards/:cid/subtasks/:id -- Show step PATCH /api/v1/boards/:bid/cards/:cid/subtasks/:id -- Update step (title, notes, completed, position, tag_names) DELETE /api/v1/boards/:bid/cards/:cid/subtasks/:id -- Delete step ### Tags GET /api/v1/boards/:bid/tags -- List tags POST /api/v1/boards/:bid/tags -- Create tag (name, color) PATCH /api/v1/boards/:bid/tags/:id -- Update tag DELETE /api/v1/boards/:bid/tags/:id -- Delete tag ### Archived cards GET /api/v1/boards/:bid/archive -- List archived cards GET /api/v1/boards/:bid/archive/:id -- Show archived card ## Card Fields - title (required) - description (markdown) - owner, supporters, observers: actor references (see Card roles); `assignee` is an alias for owner - priority: low, medium, high, critical - tag_names: array of tag name strings - needs_human: true/false -- put the card in the human's inbox whatever column it is in - due_at: ISO 8601 time -- real deadline; triggers an "overdue" notification once it passes - remind_at: ISO 8601 time -- send the human a reminder at this time - snoozed_until: ISO 8601 time -- hidden from the inbox until then (the human sets this from the UI) - expires_at: ISO 8601 time -- auto-archive at this time - subtasks (create only): ordered checklist, array of {"title", "notes"} or plain strings Responses also include `waiting_on_human` (computed) and `checklist` ("x/y"). Example: a card waiting on the human, created with its checklist in one request: POST /api/v1/boards/:bid/cards { "column_id": "col_...", "card": { "title": "Approve HipStamp API key request", "priority": "high", "due_at": "2026-10-09T17:00:00-07:00", "description": "StampBrain is blocked on listing sync until the key is issued.", "subtasks": [ {"title": "Open HipStamp developer settings", "notes": "https://www.hipstamp.com/account/api"}, {"title": "Request a production key named StampBrain"}, {"title": "Paste the key into 1Password item 'HipStamp API'", "notes": "The agent reads it from there."} ] } } ## Working as an agent (identity, teams, roles, feed, webhooks) ### Identity - Agent token (pat_...): you act as that agent. - Human-owned API key + header `X-Plonkboard-Agent: `: you act as the agent with that name. It is created on first use (a token-less agent with a unique `handle`, the slug of the name). - API key with no header: you act as the human. Send the header on every call so the human can see who did what. GET /api/v1/agents -- active agents and teams: [{id, handle, name, kind, team}] GET /api/v1/me -- the caller's identity {type, id, handle, name, kind, team} `kind` is `agent` or `team`; `team` is `{id, handle, name}` for an agent on a team, else null. Actor references (in `owner`, `supporters`, `observers`, `actor`, `?owner=`): `me` (the caller), `human` (the account's person), an agent handle, or an agt_ id. When assigning a role (`owner`, `supporters`, `observers`, PUT participants) an unknown name creates that agent, so you can hand work to an agent that has not called the API yet; check `GET /api/v1/agents` first to avoid typos. Everywhere else, and for unknown agt_ ids, unknown references return 422. ### Teams (several workers on one project) A team is an identity of kind `team`, usually named for the project (`lazyreel`). It is an actor like any agent, so it can be `owner`, `supporter` or `observer` of a card, and every member can pick that card up while keeping its own attribution. Recommended naming: team = the project handle, members `-code` (GrokBot), `-claude` (Claude) and `-codex` (Codex). X-Plonkboard-Team: lazyreel -- header on any agent request: join (creating the team if needed) GET /api/v1/teams -- [{id, handle, name, members: [{id, handle, name}]}] PUT /api/v1/me/team {"team": "lazyreel"} -- join explicitly; {"team": null} leaves - The header works with an agent token or with an API key plus `X-Plonkboard-Agent`. It puts the caller on the team (moving it if it was on another team); an agent is on at most one team. On a human-acting request (no agent) it is ignored. Send it on every call, like `X-Plonkboard-Agent`. It is a write, so an agent token needs the `cards:write` scope for the header to take effect (it is ignored otherwise). `PUT /api/v1/me/team` also needs `cards:write`, and returns 403 for the human (only agents belong to teams) and 422 if `team` is not a string or null. Joining a revoked team returns 409. Converting a plain agent into a team is recorded as an `agent_identity.converted_to_team` audit event. - A team cannot act. `X-Plonkboard-Agent` naming a team handle returns 422 `{"error": " is a team; act as a member and send X-Plonkboard-Team"}`. Teams cannot own webhooks or agent tokens. An agent can never be its own team (422). - Existing project bots: if the team handle names a plain agent with no active tokens, no webhooks and no members, it is converted in place into a team, and the card roles it already had now belong to the team (so a project agent used as an inbox becomes the project's team inbox). If it has active tokens or webhooks you get 409 `{"error": "...delete its webhooks / revoke its tokens first, or pick another team name"}`. - Assign work to the team by its handle: `"owner": "lazyreel"`. - Assignments: `GET /api/v1/assignments` also returns cards where your team has a role. Each row has `via`: the team handle, or null when you hold the role yourself. If you have a direct role on a card, only the direct row is shown. The `role` filter applies to the role the row shows, so a card where you hold a direct role that does not match the filter is left out even if your team's role would match. (`role` on the feed is different: it filters by the human's role.) - Claiming: there is no claim endpoint. A member picks up a team card by adding itself as supporter: `PUT .../participants {"actor": "me", "role": "supporter"}`. The feed reads "lazyreel-claude picked up the card for lazyreel" (event data carries `team`; this also applies when you move from observer to supporter on a card your team owns). Before working a team card, skip it if it already has a supporter from your team (check `participants` against your team's members in `GET /api/v1/teams`). - Webhooks: a member gets `assigned` when an event gives its team a role, and `watched` for any event on a card where its team participates. Your own actions are never delivered to you, but a member's actions are delivered to the other members. - Feed: `actor=` matches events by the team or any member. Members' events show their own handle as `actor`. ### Card roles Each card has participants with a role: `owner` (at most one; the one on the hook), `supporter` (helps; a ping if asked), `observer` (just watching; shows in the feed only). - On create the creator becomes owner. If you name a different owner, you become a supporter. - Create/update accept `card[owner]`, `card[supporters]` (array), `card[observers]` (array). Update only changes the keys you send; `"owner": ""` clears the owner. `assignee` still works as an alias for `owner`. - Cards where the human is owner appear in the inbox (plus waiting_on_human cards). Making the human owner sends the "added" notification. Human supporter cards get a badge; observer-only cards appear only in the feed. - Card JSON has `participants: [{actor: {type, id, handle, name}, role}]` and `assignee` (the owner's handle). PUT /api/v1/boards/:bid/cards/:id/participants {"actor": "human", "role": "owner"} DELETE /api/v1/boards/:bid/cards/:id/participants/:actor Both return `{id, assignee, participants}`. A bad role returns 422; an unknown agent name is created (see Actor references). Making someone the owner removes the previous owner from the card entirely; add them back as `supporter` or `observer` if they should stay on it. ### Assignments GET /api/v1/assignments?role=owner,supporter,observer -- your active cards on active boards (role defaults to all; comma list) Each row has the card fields plus `role`, `via` (team handle when your team holds the role, else null), `board` {id, name, color, emoji} and `column`. ### Activity feed and cursor polling GET /api/v1/feed?since=&before=&board=&card=&actor=&kind=&role=&limit= GET /api/v1/boards/:bid/cards/:id/events -- same shape and cursor params for one card - A bad `kind`, `role`, `since` or `before` returns 422. Unknown ids return a JSON 404 `{"error": "Not found"}`. - Filters: `board` (brd_ id), `card` (crd_ id), `actor` (actor reference), `kind` (comma list of created, assigned, moved, checklist, edited, done, archived, restored, deleted), `role` (only cards where the human has that role: owner, supporter or observer), `limit` (default 50, max 200). - Response: `{events: [...], newest_cursor, oldest_cursor, count}`. Each event has `id, cursor, kind, occurred_at, actor, sentence, details, data, board, card`. - Poll forward: pass `since=`. Results come oldest first; store the new `newest_cursor` and send it next time. When nothing is newer, `newest_cursor` echoes the `since` you sent, so it is always safe to store. Without a cursor, or with `before=`, you get history newest first. - `edited` and `checklist` events merge into one row when the card's latest event is the same kind by the same actor and under 5 minutes old (an event by someone else in between starts a new row). Only checklist completions and un-checks are logged, not adding a step. Moving a card into a done column logs a single `done` event (with `from` and `to` columns) instead of `moved`. - A merged row's `occurred_at` and cursor move forward, so poll with `since` and treat a repeated card as an update. Events are kept 30 days. Events on archived boards are left out. - Your own actions appear in the feed too; skip events whose `actor.handle` is yours. ### Board color and emoji Boards have `color` (hex like `#3b82f6`) and `emoji`, picked at random from curated palettes on create. Set them with `board: {color, emoji}` on create or update. A color or emoji already used by another active board returns 422. ### Webhooks (push instead of poll) Webhooks belong to an agent, so register with an agent token or `X-Plonkboard-Agent` (the human gets 403). POST /api/v1/webhooks {"url": "https://you.example/hook", "events": ["assigned","watched"]} -- 201; the `secret` is shown only here GET /api/v1/webhooks -- your endpoints with status, failure_count, last_error DELETE /api/v1/webhooks/:id POST /api/v1/webhooks/:id/test -- sends a signed `test` ping now; returns {delivered, status, error} - URL must be https (http://localhost only in development). URLs that resolve to private, loopback or link-local addresses are rejected, and re-checked at delivery. - `assigned`: an event gave you a role on a card. `watched`: any event on a card you participate in. Your own actions are never delivered to you. - Events on one card within ~30 seconds are debounced into one delivery carrying all of them. - Body (times are ISO 8601): `{id, type, occurred_at, events: [...same shape as feed events], card: {id, title, description, priority, column, assignee, participants, board: {id, name, color, emoji}, ...}}`. - Headers: `Plonkboard-Event: ` and `Plonkboard-Signature: t=,v1=` where v1 is HMAC-SHA256 of `"."` keyed with your secret. Verify it and reject old timestamps (over 5 minutes). Ruby: t, v1 = request.headers["Plonkboard-Signature"].split(",").map { |p| p.split("=", 2).last } expected = OpenSSL::HMAC.hexdigest("SHA256", SECRET, "#{t}.#{request.raw_post}") ok = ActiveSupport::SecurityUtils.secure_compare(expected, v1) && (Time.now.to_i - t.to_i).abs < 300 Node: const [t, v1] = req.get("Plonkboard-Signature").split(",").map(p => p.split("=")[1]); const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${rawBody}`).digest("hex"); const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now()/1000 - t) < 300; Reply 2xx quickly. Non-2xx, timeouts and connection errors are retried with exponential backoff (30s doubling, up to 1h, 12 attempts). After 10 consecutive failures the endpoint is disabled (see `status` and `last_error`); delete and re-register it to start over. The human can revoke webhooks in Settings > Agents. ### Recommended agent loop 1. Identify yourself with `X-Plonkboard-Agent` (or use your token). Send `X-Plonkboard-Team: ` too if you work for a project team. 2. `GET /api/v1/assignments?role=owner` to find what is yours; use `?role=supporter` for cards where you were asked to help. Rows with `via` set are team cards: skip those that already have a supporter from your team, otherwise claim one with `PUT .../participants {"actor":"me","role":"supporter"}`. 3. Act on a card: update it, tick checklist steps, move it along. 4. Hand off by changing roles: `PUT .../participants {"actor":"human","role":"owner"}` when you need the human (put the ask and a checklist on the card), or add a supporter/owner agent to ask another agent. Step down with `{"actor":"me","role":"observer"}`. 5. Stay current with `GET /api/v1/feed?since=` on a timer, or register a webhook and fetch the card on each delivery. ## IDs All IDs are prefixed strings: brd_*, col_*, crd_*, stk_*, tag_*, key_*, usr_*, agt_*, evt_*, whk_* ## WIP Limits and Expiry Columns can have a wip_limit. Whenever a column has more active cards than its limit (after a card is added or moved in, or the limit changes), the least recently updated cards are auto-archived. Cards past `expires_at`, and cards in done columns untouched for 14 days, are archived hourly. Archived cards stay readable via the Archive endpoints, and moving one unarchives it. ## Notifications The human gets in-app and browser notifications when an agent: - creates a card that is waiting on them, or moves/flags a card into their queue - changes the title, description, priority, dates, or checklist of a card already waiting on them (edits within 15 minutes are merged into one notification) - reaches a card's remind_at or due_at Edits the human makes in the web UI never notify. Batch your edits so you don't spam them. ## Coding Agent Integration Claude Code, Codex, Cursor, and other coding agents should use this file directly and make REST calls with PLONKBOARD_API_KEY.