Skip to content

The agent loop

Every tool description teaches the workflow, the server’s instructions spell it out, and most results end with the next step, so most agents follow this loop without extra prompting. If you’re writing your own agent, this is the shape to follow.

{ "name": "introduce", "arguments": { "name": "auth-refactor", "where": "api · feat/auth", "project": "api" } }

Use the name your human already knows the session by: its title in the CLI or app, the terminal tab, or the branch. The board shows it on the cards you hold and in its list of agents, so someone running ten Codex windows can tell which is which. Names are unique among live agents; if yours is taken, you get a numbered one (auth-refactor-2). Leave the name out and the board picks one, like jade-heron. Either way, title your session with the name you got if your client lets you, so the app and the board agree. It works the other way too: when the human asks “what is auth-refactor doing?”, any agent can answer with find_cards and held_by.

The human can rename an agent from the board’s list of agents. Its next tool results then carry the new name and ask it to retitle its session; the news repeats on reads and stops after the first call that changes something.

Some clients keep no MCP session (the Claude apps’ connector, for one), so every chat on a token reaches Yokka as one connection. There, introduce makes each chat an agent of its own and answers with an as name: pass it to claim_card (and to find_cards with held_by_me) so the board knows which chat holds which card. Tools that name a card you hold act as you without it. See Subagents and helpers.

project is optional: it makes that project your session’s default, so tools that take a project use it when you leave it out. Claiming or filing a card in another project moves the default there. On a shared connection the default is your own chat’s, and applies to calls made with your as name.

Call list_projects if you don’t know which project to work in, then get_board to see its swimlanes, lanes and cards. With more than one project and no default yet, name it on every tool that takes one: reads use the oldest active project, but tools that change the board refuse to guess, and so does get_next_card.

Track the work your human asks for on the board. Before starting a task, find its card with find_cards or take the next one with get_next_card; if they ask for something that isn’t on the board yet, file it with add_card and claim it before you start. Questions, explanations and quick lookups don’t need a card. The server’s instructions say this to every agent, so it holds without an AGENTS.md in the repo.

{ "name": "get_next_card", "arguments": { "swimlane": "Bugs" } }

get_next_card returns the top unclaimed card waiting to be picked up. Leave out swimlane to search every swimlane from top to bottom. Cards blocked by work that isn’t done yet are skipped. It only looks: to take the next card in one step, call claim_card without a card (step 3). Then read the brief in full:

{ "name": "get_card", "arguments": { "card": "YK-12" } }

Besides the brief and checklist, get_card carries what earlier agents left for you:

  • Handoff note: what the last agent to let go of the card wrote for the next one (see step 9).
  • Delivered upstream: for each card this one waits on, its completion summary and links, its pull requests and its branch, so you can build on that work without reading every blocker.
  • Project notes: the notes people pinned, then the ones that best match the card, then the latest, up to 3, each cut to 400 characters, each marked as from a person or from an agent (see Project notes).
{ "name": "claim_card", "arguments": { "card": "YK-12", "note": "Reproduce, then fix the date parsing" } }

Claim a card when you start on it, not a batch up front: the board then shows what you’re actually doing, and cards you haven’t reached stay free for other agents. The board moves the card (usually to Working) and shows your agent on it. The claim is refused if another active agent holds the card, or another live session of the same agent (same token and client) does, such as a second window. A blocked card is refused too, naming the cards it waits on; pass force: true only when the human asked you to work on it anyway. If a runner started you, pass the launch code from your start prompt (like r_7f3k2a) as launch: it links your session to that run.

When you plan work that has to happen in order, say so on the board: file each card with blocked_by listing the cards it waits on (update_card takes add_blocked_by and remove_blocked_by too). Agents then pick the cards up in order, and several can work the plan in parallel without starting anything too early.

Leave out card to claim the next card waiting, exactly as get_next_card would pick it (it takes the same project, swimlane and epic), in one step: cards other agents hold are skipped, so two agents asking at once never get the same card. The reply names the card it claimed and includes its brief, or says nothing is waiting. Either way, a claim’s reply also carries the card’s handoff note and its project notes, so you read them before you start.

{ "name": "claim_card", "arguments": { "swimlane": "Bugs" } }

When nothing is waiting, get_next_card and claim_card without a card can wait for a card instead of answering “nothing waiting”. Add wait_seconds, up to 50:

{ "name": "claim_card", "arguments": { "swimlane": "Bugs", "wait_seconds": 50 } }

The call answers as soon as a card is ready to pick up: a card filed or moved into the lane agents pick up from, a card’s last blocker reaching a done lane, a card released by its agent or let go by the board. It answers with exactly what a call without the wait would, with the same project, swimlane and epic filters, and counts as one call toward the plan’s allowance however long it waits (the token’s rate limit is charged one more call for every 10 seconds of waiting after the first). claim_card claims the card it finds in the same step, so when several agents wait at once each card goes to one of them, and the others keep waiting. If nothing turns up, the reply says to call again.

That makes a “keep working until I stop you” loop:

  1. claim_card without a card, with wait_seconds: 50.
  2. If it claimed a card, work it through (steps 4 to 9) and go back to 1.
  3. If it timed out, go back to 1.

Leave several agents running this loop and they pick up work as you file it, and a card that waits on others is picked up as soon as they’re done.

The claim lasts while you keep calling Yokka: any call keeps it alive. After the project’s stale time (30 minutes by default) without a call it lapses: the board flags the card as stuck and another agent may take it. After twice the stale time the board releases the card. Building a feature can take longer than that with no single “long step” in it, so tick each checklist item as you finish it and call report_progress at least every half stale time (15 minutes by default). The claim’s answer states these minutes, and an agent that comes back to a card flagged stuck is told again. It doesn’t lapse while a question you asked with request_input waits on the human; once they answer, the minutes count from their answer. After a restart, find_cards with held_by_me shows the cards you still hold.

A helper’s claim is its own: only calls that act as the helper (naming its card, or passing its as) keep it alive. The parent’s other calls don’t.

Subagents share their parent’s connection, and so do the chats of a client that keeps no MCP session (the Claude apps’ connector, for one). Yokka can’t tell them apart by the request, so each names itself when it claims:

{ "name": "claim_card", "arguments": { "card": "YK-12", "as": "date-parsing" } }

The board then shows date-parsing as its own agent, listed under the session that started it, with its own claims. Every tool that names a card it holds (report_progress, check_items, complete_card…) acts as it, so it only passes as when it claims. Use the same name for every card it claims; find_cards with held_by_me and as lists just its cards, and without as lists every helper’s. A helper renames itself with introduce, passing its current name as as and the new one as name. A session can have up to 20 helpers; reuse a name rather than making new ones.

Before you change anything, write your plan as the card’s checklist. Only a one-line fix may skip it:

{
"name": "set_checklist",
"arguments": { "card": "YK-12", "items": ["Add a failing test for ISO week dates", "Fix the parser", "Update the changelog"] }
}

The card shows 0/3 and a progress bar that follows the items. Tick each item off when it’s really done, by the number set_checklist and get_card show:

{ "name": "check_items", "arguments": { "card": "YK-12", "items": [1] } }

If the card already has items when you read it, the human wrote them: they’re the acceptance criteria. Keep them (set_checklist never drops them) and work through them. Call set_checklist again with the full list to revise your plan; unchanged items keep their number and tick, and your items that aren’t in the new list are dropped. Pass done: false to check_items to untick an item. When complete_card succeeds, its reply lists any items still open.

{
"name": "report_progress",
"arguments": { "card": "YK-12", "message": "Added a failing test for ISO week dates", "step": "Tests" }
}

The human watches the board live, so keep each message to one short line and only report work you’ve done. On a card without a checklist you can pass a rough percent; with one, the bar follows the items.

When a long step starts (a test suite, a build, a deploy), say so, and say how it ended. Otherwise the board shows your last milestone for as long as the step runs, and the card looks stuck.

If you hand part of a card to a subagent, give it the card’s ref and have it call check_items and report_progress on that card itself. The human only sees what reaches the board: a helper that works for 25 minutes without a call looks, on the board, like nothing happening. Keep each card’s changes able to ship on their own, so you can complete one card while others are still in progress.

{
"name": "request_input",
"arguments": { "card": "YK-12", "question": "Should week 53 roll over to week 1 of next year, or stay 53?" }
}

The card is flagged for the human and usually moves to Needs you. The reply includes a cursor. Wait for the answer by reading only what happens after it, with wait_seconds:

{
"name": "get_card_activity",
"arguments": { "card": "YK-12", "after": "<cursor from request_input>", "wait_seconds": 50 }
}

The call answers as soon as a person (or another agent) adds something after the cursor, or after up to 50 seconds, and counts as one call however long it waits. A commented entry from a person is their answer. If there’s nothing yet, call it again with the cursor it returned. If the human answers you somewhere else (in your chat), note their answer with comment_card and "answers_question": true before carrying on: that takes the “your turn” flag off the card. Moving the card on yourself (say back to Working) does the same. Use comment_card to reply or leave a note without flagging the card again.

When the human should pick between a few choices, pass 2 to 6 of them as options so they can answer with one tap. For visual choices, attach each image with attach_file first and name the files in option_files: one entry per option, in the same order, with "-" for an option without a file. Their reply’s picked says which option they chose, counting from 1. The human can always pick Other and write their own answer instead; that reply has no picked.

When you can, attach proof: a screenshot of the change, a short screen recording or a test report.

{
"name": "attach_file",
"arguments": { "card": "YK-12", "filename": "week-53-tests.txt", "caption": "Year-boundary tests passing" }
}

You must hold the card. The reply is a single-use upload link: PUT the file’s bytes to it within 15 minutes and it appears on the card. Images, MP4, WebM and MOV videos, PDFs and text files are accepted, up to 20 MB each (less on some plans: the reply gives the cap). If a runner started you and your sandbox can’t reach the network, pass path (relative to your working folder) instead, and the runner uploads the file. Attach before you complete the card.

If your app tells you what the work cost, send it with report_usage before you complete the card: tokens, the cost in US dollars, the model and how long you worked. The card shows its total, and on Team and up the workspace adds it up by epic, project, person, agent and month.

{
"name": "report_usage",
"arguments": {
"card": "YK-12",
"model": "claude-opus-4-5",
"input_tokens": 1200,
"output_tokens": 48000,
"cache_read_tokens": 2100000,
"cache_write_tokens": 96000,
"cost_usd": 3.81,
"duration_ms": 1260000
}
}

Send your running totals for the card, not just the latest step: each report replaces your last one for that card, so you can report after every milestone and nothing is counted twice. Each number is optional, but send at least one. Runs a runner started report their usage on their own, and the runner’s numbers win over yours for the same run.

{
"name": "complete_card",
"arguments": {
"card": "YK-12",
"summary": "ISO week parsing now handles week 53; added tests for year boundaries.",
"links": ["https://github.com/acme/app/pull/481"]
}
}

The board moves the card on, usually to Review (or Verify for bugs), where the human checks it before it’s shipped. The summary and links also reach the cards waiting on this one: their get_card lists them under what was delivered upstream.

If you can’t or shouldn’t finish, use release_card with a one-line reason instead, and the card goes back to be picked up again. Leave a handoff note for whoever picks it up next: what you did, what’s left, where you stopped.

{
"name": "release_card",
"arguments": {
"card": "YK-12",
"reason": "Out of time",
"handoff": "The parser fix is on branch fix/week-53; the year-boundary test still fails for 2027."
}
}

The next agent gets the note when it claims the card, get_card shows it, and the human sees it on the card. A later handoff replaces it; completing the card clears it.

Before you stop, leave every card you hold completed, released, or waiting on the human with request_input; find_cards with held_by_me lists them. A session that ends without doing so leaves its cards claimed until the board lets them go: when its client closes the MCP session, or once the claim has been stale for twice the project’s stale time.

Found something out of scope? File it rather than doing it:

{
"name": "add_card",
"arguments": {
"title": "Date picker ignores the workspace locale",
"brief": "**What:** …\n\n**Why:** …\n\n**Done when:** …",
"swimlane": "Bugs",
"labels": ["frontend"]
}
}

Notes are what the next agent on the project should know: a decision and why, a convention, a gotcha, where something lives. They outlive your session, and every agent gets the relevant ones with the cards it reads and claims.

{
"name": "add_note",
"arguments": {
"title": "Use pnpm, not npm",
"body": "npm install rewrites the lockfile and CI fails.",
"card": "YK-12"
}
}

Keep one idea per note, with a short title and a few lines. Search before you add, and rewrite a note that no longer holds instead of adding another:

{ "name": "find_notes", "arguments": { "text": "lockfile" } }
{ "name": "update_note", "arguments": { "note": 3, "body": "pnpm 10: the lockfile is pnpm-lock.yaml." } }

Only people pin notes, in Project settings → Notes: a pinned note comes first wherever agents are given notes, so it’s how the team states its rules. Agents can’t pin or unpin a note, and pinned is refused. Agents can change notes agents wrote, unless a person pinned one; notes a person wrote are theirs. Tool results label each note as from a person or from an agent, and a note from an agent is what an earlier agent learned, not a rule. A project keeps up to 200 notes, on every plan. The human sees, edits, pins and deletes them in Project settings → Notes. Notes aren’t for progress (report_progress) or for questions to the human (request_input).

Leave every card you hold in a state the board can explain: completed, released, waiting on the human with request_input, or with a report_progress line saying what it waits on (“e2e suite running in the background”). A card left in Working with a stale message is the one thing the human can’t tell apart from a stuck agent.

stop_check finds those cards: the ones you hold that aren’t done, released or waiting on the human, and that you haven’t said anything about for 10 minutes or more. It answers {} when there are none, and otherwise:

{
"decision": "block",
"reason": "Before you stop: the board shows this card as in progress, with no word from you for a while.\n- YK-12 \"Fix week 53\" (Working, last update 22 minutes ago: \"Running the e2e suite\")\n..."
}

That’s Claude Code’s Stop hook format, so Claude Code can run the check for you at the end of every turn: see the Claude Code tab of the Quickstart. The hook sends the agent back once per turn at most (stop_hook_active is true the second time, and then the check lets the turn end), and if the connection is down the turn ends as usual. Agents in other apps can call stop_check themselves before they stop.

stop_check only looks at the cards your own session holds. It takes no as, so cards your helpers hold never show up in it: before you stop, check that each subagent you started left its cards completed, released or waiting on the human (find_cards with held_by_me lists your helpers’ cards too).

See the tool reference for every argument.