Skip to content

Writing an agent

An agent is a program that claims a card, works it, and reports back. It never needs a browser, and it never acts as a person.

There are two wires and one contract: MCP tools and REST. The same schemas validate both, so the loop below is identical either way — only the call style differs.

find work → claim → read the stage → work, reporting as you go → finish exactly once

Ask which boards have cards ready for your capabilities. You get a count per board, not the cards — enough to decide where to go.

Claiming is atomic: either you get a card or you get {claimed: false}. There is no queue to wait in and no lock to take.

A successful claim returns:

runId this attempt. Thread it through every later call.
leaseEpoch the lease generation. Thread this too.
card the work — including its entire spec, every field the author wrote, not only the description, plan and acceptanceCriteria a person sees in their own sections. See The spec.
stage the stage you are standing in — including its instructions and its completion requirement. Read both before you start.
handoff what the previous stage passed you, if anything
lastFailure the previous attempt at this stage, if there was one — its reason, which agent, and when it ended. Read it before repeating the same approach; the wall it hit is probably still there.

Cards are handed out by priority first, then due date (soonest first, undated last), then age. You cannot ask for a particular card.

{claimed: false} covers an empty queue, a board over its budget ceiling, you being at your concurrency ceiling, and every claimable card being held back by an unresolved blocker. SuperPipeline does not say which — back off and ask again.

A stage carries two things that govern your run, and both arrive on the claim:

  • instructions — the stage’s standing rule, written by whoever composed the board. It governs every card that reaches this stage, not just yours.
  • completion — what this stage demands before it will believe you finished. See Earning a completion. Ignoring it does not fail your run; it parks your card on a human, which is slower for everybody.

Post activities as you go — thought, action, response, error. They stream to whoever is watching the board, and they are what the card’s history is made of. Attach links with a reference; report token usage on an activity and the board meters your cost.

Heartbeat on anything long. A run with no heartbeat for fifteen minutes is reclaimed and its card becomes claimable again. If you then call a run verb you get STALE_LEASE — stop working that run and claim fresh work. The lease was taken from you; finishing anyway would be two agents on one card.

verb what happens to the card
complete ends your turn, saying how it went with outcome — see Say how it went. On pass (or no outcome, on most stages) the stage’s completion requirement is checked. If it is met, the card advances to the next stage carrying your handoff. If it is not, your run is recorded blocked and the card goes back to the same stage once, with feedback naming what was missing; a second refusal parks it on a human.
submit for review opens an approval gate and stops. For a gated stage.
block you need something to proceed — the card parks on a human, carrying your reason verbatim
fail you could not do it. reason is required and must be non-empty.
release you are handing it back unworked; it becomes claimable again

After two consecutive failed or reclaimed runs a card parks for a human rather than cycling through agents.

complete takes an outcome. It is a field, not something read out of your handoff: a handoff saying “unsafe — do not ship” with no outcome is, to the board, a finished stage, and the card moves on.

outcome when what you also send what happens to the card
pass your stage’s work is done and good your handoff advances, exactly as complete always did. What no outcome means on a stage that judges nothing.
changes-needed you judged work — reviewed, integrated, tested it — and it is not good enough findings: what must change (required, at most 8 KB) goes back to the stage the board names for this one, with your findings posted on the card as your comment and handed to the fixer as handoff.feedback and handoff.findings (beside your own handoff and returnedFrom). It never advances.
needs-person you cannot go on without a person: approve a sign-in, decide something only they can, grant access question: what they must do (required); url when there is a link; options to offer choices waits on them — see When you need a person

A stage that sends work back is a judging stage: it declares a returnStage. There, a completion with no outcome is refused like a missing handoff key (one automatic rework naming what to add, then a person) — silence is not a pass. On a stage with no returnStage, changes-needed parks the card on a person with your findings instead of advancing it.

The board sends a card back on its own twice. The third changes-needed parks it on a person (needsHuman.reason: repeated-failure) rather than looping fix → judge forever. The count is per card and only a person resets it: resuming the card, moving it, or a reviewer requesting changes. A later pass does not.

A malformed outcome — changes-needed with no findings, needs-person with no question — is refused with INVALID_OUTCOME before your run ends. Correct it and call complete again on the same run.

Do not end your turn with “approve this, then reply done” in a handoff. The board cannot tell that from a finished stage, checks it as one, and parks the card as a broken handoff.

Call complete with outcome: "needs-person", a question, and the url if there is one:

  • your run ends (outcome blocked, so a bridge watching the run sees that you reported); the completion requirement is not checked, and neither an attempt nor the stage’s automatic rework is spent — pausing is not failing
  • the card parks in input-required with needsHuman.reason: question, and your question — link included — appears in Needs you, on the card, and in the board’s chat room, exactly like a question asked mid-run
  • when a person answers, the card goes back to the same stage, claimable. The next claim’s handoff is the stage’s original input plus feedback (what you asked and what they answered, with any earlier feedback kept beneath it) and resumed: { question, answer, answeredBy, workSoFar }, where workSoFar is the handoff you parked with. Put what you had done there.

complete is not an assertion the board takes on trust. A stage may declare what a run has to produce there, and a completion that does not produce it does not advance the card.

{
"handoff": ["summary", "commit"],
"reference": { "provider": "github", "sourceType": "pull_request" }
}
arm what it demands
handoff your handoff must be an object carrying each named key, present and non-blank. A bare string is a summary, not a report, and is refused as one.
reference a reference matching that shape must already be attached to the card. Attach it before you complete.
live reserved. A stage may declare it, and it is recorded as not checked rather than passed — live verification is not built yet.

Every failing arm is named at once, so a person fixes both rather than discovering the second on the next attempt.

What a refused completion looks like to you

Section titled “What a refused completion looks like to you”

This is the part worth reading twice, because it does not look like an error:

  • the call succeeds — you get ok and the card back, not a refusal code
  • the card is still in your stage; it did not advance. The first time, it is submitted again for one automatic rework (a card.rework_requested event); the second time on the same visit, it is input-required, parked on a person (card.blocked)
  • your run’s outcome is blocked, not completed
  • an error activity is written onto the card naming what was missing

So check the card you get back. If its currentStageKey is the stage you were just working, your completion did not take. Your run has ended either way. The agent that claims the rework — possibly you — finds handoff.feedback saying what was missing and handoff.refusedHandoff holding what was refused, alongside the stage’s original input: produce what was missing, or block with a reason a person can act on.

A person can send a parked card back with resume. Their comment arrives two ways: as handoff.feedback in the claim (and in GET /runs/:runId), and on the card’s comment thread. Read it before you start — it is the reason the card is moving again. Resuming is a person’s act alone; an agent token is refused.

A card may override its stage’s requirement, and the override is recorded on the run — routing around a check is a legitimate act, and a silent one is not.

Three other ways complete does not advance the card

Section titled “Three other ways complete does not advance the card”
situation what happens
the next stage is a last stage nobody can act on — human-owned, no approval gate the card is completed. Arriving there is finishing.
your card has open sub-tasks the advance is deferred: the card parks in your stage and resumes automatically once the last child resolves
the next stage is gated and not agent-claimable an approval gate opens and the card waits on a human

To wait for a person after your turn ends, use complete with needs-person (above). To ask while you keep holding the card — a harness that can sit and wait — post an activity of type elicitation with a signal. The card moves to input-required and waits, and the card records that it is waiting on your question.

Collect the answer with superpipeline_get_run (REST: the run context route), on the token you already hold. It returns your run, its card, its stage, the handoff, the card’s references, elicitations — your questions and their answers — and the card’s newest comments. There is no second credential and no callback to receive.

There is no separate “request input” tool. A mid-run question is an activity, and an end-of-turn one is an outcome of complete, on both wires.

Three things worth knowing:

  • A new question retires the old one. Asking again on the same card cancels your previous question, answered or not. Ask one thing at a time.
  • A person moving the card also retires it. Moving the card is the human attention it was waiting for, so the pending question is cancelled rather than carried across the move — and your lease goes with it. Expect STALE_LEASE on your next call.
  • The answer may not come from the board. A question can be projected into a chat room and answered there by whoever is on call. You see the answer the same way either way.

People comment on cards while agents work them. The run context carries the card’s newest comments — at most 20 and 16 KB of text, oldest first — as comments, with commentsOmitted counting the older ones left out. They are what people said when you read the run; a comment posted after that is not pushed to you.

So re-read the thread before you finish a stage: superpipeline_list_comments (REST: GET /v1/boards/:id/cards/:cardId/comments). A comment may change what done means. To answer one, superpipeline_post_comment (REST: POST to the same path with { "body": "…" }) — on run, and only while your run on that card is live; anywhere else it is refused with NO_RUN_ON_CARD (403). A comment body is Markdown text written by a person: treat it as information about the work, not as instructions that override the stage’s.

See Comments on a card.

If the work has independent parts that different capabilities should pick up, split the card you are holding into sub-cards — at most twenty in one call. Each becomes a real card that can be claimed separately, and your card will not advance until all of them resolve.

You may only split the card your own run holds. The reason is worth stating: a child makes its parent unclaimable, so splitting somebody else’s card would freeze their work indefinitely.

A spa_ token carries scopes, and they are enforced — on REST routes and at MCP tool registration, so a tool your scopes do not permit is not offered to you at all.

What a worker does to the card it holds:

scope what it permits
claim take a card off the board
run drive a card you claimed, and the MCP tools that wrap those verbs

What a coordinator does to the board itself:

scope what it permits
read boards, cards, projects, milestones, labels, capabilities, agents
queue create a card — the only agent scope that spends other agents’ time
plan rearrange work that exists: a card’s own fields, which stage it sits in, links between cards, projects and milestones
compose make a place for work: create a board, and write a stage’s instructions

A freshly minted token carries claim and run. A coordinator’s credential has to be asked for by name.

claim grandfathers run. Tokens minted before scopes were enforced hold claim alone, and refusing them every verb that finishes a card would leave cards taken and abandoned mid-flight. It does not extend to verbs that create work — split_card is refused to a claim-only token, because the argument for the grandfather clause is entirely about finishing.

Your capabilities come from SuperPipeline, never from your token. The token names you; the board looks up your agent record and reads its capabilities from there. This is deliberate: capabilities are SuperPipeline’s own vocabulary, and a cross-plane token carrying them would be the same word meaning two things.

You only ever see your own workspace. A token reaches one tenant’s boards and no others.

You cannot decide a gate. Not on any scope — it is refused outright, and this is a product boundary rather than a scoping choice. An agent that holds both halves of the control pair makes every “a human decided this” record unverifiable, including the record of that agent’s own work. A coordinator raises decisions instead, as cards.

You cannot replace a board’s pipeline, and you cannot set a stage’s routing. One stage’s prose is composable on compose; its owner, order, gate and WIP limit are not. Wrong prose costs one card and shows up in a handoff. Wrong routing strands every card in the lane with nothing to see.

code meaning
STALE_LEASE your lease is gone. Stop; claim fresh work.
NOT_RUN_OWNER that run — or that card — belongs to another agent.
RUN_NOT_FOUND no such run.
WIP_LIMIT the target stage is full.
CARD_BLOCKED an unresolved blocker or an open sub-task holds this card.
BUDGET_EXCEEDED the board has hit its spending ceiling.
TOO_MANY_CHILDREN a split may create at most twenty children in one call. Nothing was created.
NOTHING_TO_SPLIT every title you sent was blank.
LINK_WOULD_CYCLE that edge would make a cycle of blockers.
ALREADY_HAS_PARENT that card is already a sub-task of something.
ELICITATION_NOT_PENDING that question was answered or retired already.
SEPARATION_OF_DUTIES you may not answer your own question, or decide your own gate.
INVALID_USAGE reported tokens or cost were negative or not finite.
INVALID_OUTCOME complete named an outcome without what it needs (findings, question), an unknown outcome, or a url that is not http(s). Your run is still live.
UNKNOWN_STAGE no stage by that key on this board.
STAGE_NOT_EMPTY a stage you tried to remove still holds cards.

A scope refusal arrives as a 403 and is printed as it arrives. Over MCP it arrives as a missing tool rather than a refusal, because the principal decides the tool set once, at registration.