REST API¶
Core’s REST API serves stateless operations
(ADR-0018: REST for Stateless, WebSocket + JSON-RPC + MessagePack for Stateful).
Base path: /api/v1.
The machine-readable contract is the OpenAPI 3 document
core/api/openapi.yaml, also served by Core (without
authentication) at GET /api/openapi.yaml. This page explains the
API; the OpenAPI document is authoritative for exact schemas.
Conventions¶
- Authentication
Every
/api/request needsAuthorization: Bearer <OIDC access token>(Configure OIDC Authentication); otherwise401.- Authorization
Every operation is authorized for the caller at platform, organization or project scope; denied operations return
403. List endpoints return only the items the caller may read.- Bodies
JSON. Unknown fields are rejected (
400). Maximum 1 MiB.- Keys
Organizations and projects are addressed by their immutable keys (
acme,ACME), not by internal IDs.- Optimistic concurrency
Every entity has a
version. Updates must send theversionthe client last read; if the entity changed since, the update is rejected with409 conflict— re-read and retry. Successful updates return the new version.- Lists
{"items": [...]}.
Errors¶
{"error": "conflict", "message": "update project: conflicting concurrent update"}
Status |
|
Meaning |
|---|---|---|
400 |
|
Malformed body or invalid value; |
401 |
|
Missing or invalid token |
403 |
|
Not allowed for this caller and scope |
404 |
|
No such entity or endpoint |
409 |
|
Key already taken |
409 |
|
Stale |
500 |
|
Server error (details only in logs) |
Identity¶
GET /api/v1/meThe caller as Core sees it:
{"subject", "email", "name", "groups"}.
/me also returns bindings: the role bindings that apply to the
caller (see below for the representation).
Roles and role bindings¶
GET /api/v1/roles → 200 list of {"role", "actions"}
Binding representation:
{"id": "0199…", "claim": "groups", "value": "acme-devs", "role": "engineer",
"scope": "organization:acme", "bootstrap": false, "created_at": "…"}
scope is platform, organization:<key> or project:<key>.
GET /api/v1/role-bindings→200listBindings the caller may read (
role_binding.readat the binding’s organization, or platform), bootstrap bindings first.POST /api/v1/role-bindings—{"claim", "value", "role", "scope"}→201Requires
role_binding.manageat the scope.platform-adminonly atplatform;organization-adminnot at project scope. The organization/project must exist (404); duplicates are409.DELETE /api/v1/role-bindings/{id}→204Bootstrap bindings are not stored and return
404.
Credentials¶
Provider API keys used by the LLM gateway
(ADR-0011: LLM Gateway for Credentials and Usage Metering)
and the token runs use for the project repository.
An organization has a default per provider; a project may override it.
Permission: credential.manage (organization admins, platform admins).
Representation — the key itself is never returned:
{"provider": "anthropic", "project": "WEB", "base_url": "",
"fingerprint": "9f86d081…1234", "updated_at": "…"}
provider is anthropic (messages), openai (OpenAI-compatible
API, used for embeddings) or git (token for cloning and pushing the
project repository, delivered only to runs —
Workspace preparation). project is absent for the organization
default.
GET /api/v1/organizations/{organization}/credentials → 200 list
PUT /api/v1/organizations/{organization}/credentials/{provider}—{"api_key", "base_url"?}→200Sets or replaces the organization default.
PUT /api/v1/projects/{project}/credentials/{provider}—{"api_key", "base_url"?}→200Sets or replaces the project override.
DELETE on either path → 204; 404 when nothing is set there.
Knowledge¶
The organization’s knowledge space, stored by the Knowledge service and
reached through Core
(ADR-0022: Humans Reach Knowledge Through Core).
Permissions: knowledge.read (all roles), knowledge.write
(engineers, organization admins, platform admins), at the organization.
Representation:
{"id": "0199…", "kind": "decision", "title": "Use SQLite first",
"body": "Easy development.", "projects": ["WEB"], "items": ["WEB-3"],
"version": 2, "created_by": "8751…", "updated_by": "8751…",
"created_at": "…", "updated_at": "…"}
kind is document, decision, note or debt; body is
Markdown; items link tracker items. created_by/updated_by are
the subjects of the humans (or agents) who wrote it.
GET /api/v1/organizations/{organization}/knowledge/entries→200listFilters:
kind,project,item. Most recently updated first.
POST /api/v1/organizations/{organization}/knowledge/entries — {"kind", "title", "body"?, "projects"?, "items"?} → 201
GET /api/v1/organizations/{organization}/knowledge/entries/{entry} → 200
PATCH /api/v1/organizations/{organization}/knowledge/entries/{entry}—{"version", …changed fields}→200Every update creates a new immutable version.
GET /api/v1/organizations/{organization}/knowledge/entries/{entry}/versions→200All versions, newest first.
GET /api/v1/organizations/{organization}/knowledge/search?q=…→200Hybrid search: full-text (FTS5, BM25) and semantic similarity, fused by reciprocal rank. Filters:
kind,project;limit(default 20, max 50). Result:{"items": [{"entry": {…}, "score": 0.0328}]}, best first. Query text is treated as plain words (no search syntax).
LLM usage¶
GET /api/v1/projects/{project}/usage→200Token usage recorded by the LLM gateway. Query:
group_by=ticket(default),modelorrun(the gateway’s caller:run:<id>for an agent session,planner:<session>for the planner);ticketlimits it to one ticket;since(RFC 3339). Permission:tracker.read. The ticket page’s timeline usesgroup_by=run&ticket=<key>.{"group_by": "ticket", "items": [{"key": "WEB-7", "requests": 2, "input_tokens": 24, "output_tokens": 10, "cache_read_tokens": 6, "cache_write_tokens": 0}], "total": {"requests": 2, "input_tokens": 24, "output_tokens": 10, "cache_read_tokens": 6, "cache_write_tokens": 0}}
Skills¶
Agent skills (Skills,
ADR-0010: Central Skill Registry). A skill is
identified by its scope and name and has an editable draft and
immutable published versions. Permissions: skill.read (all
roles), skill.write (platform admins anywhere; organization admins within
their organization and its projects).
Skill representation (the draft):
{"id": "0199…", "scope": "project:WEB", "name": "code-review",
"description": "Review checklist for WEB", "body": "# Review\n…",
"files": {"scripts/check.sh": "…"}, "latest_version": 2,
"created_at": "…", "updated_at": "…", "version": 5}
scope is platform, organization:<key> or project:<key>;
names are 2–64 lowercase letters, digits and single hyphens and unique
per scope; files are supporting text files (clean relative paths,
not SKILL.md; at most 50 files, 1 MiB in total).
GET /api/v1/skills?scope=… → 200 list of the skills defined at exactly that scope
POST /api/v1/skills — {"scope", "name", "description", "body"?, "files"?} → 201
GET /api/v1/skills/{skill} → 200
PATCH /api/v1/skills/{skill}—{"version", "description"?, "body"?, "files"?}→200Edits the draft only.
POST /api/v1/skills/{skill}/publish—{"version"}→201Snapshots the draft as version
latest_version + 1:{"number", "description", "body", "files", "published_by", "published_at"}.
GET /api/v1/skills/{skill}/versions → 200 (newest first) and
GET /api/v1/skills/{skill}/versions/{number} → 200
GET /api/v1/projects/{project}/skills→200The project’s effective skills: published skills of the platform, the project’s organization and the project, the most specific scope winning per name, with the project’s pins applied:
{"items": [{"name": "code-review", "skill_id": "0199…", "scope": "project:WEB", "version": 1, "latest_version": 1, "pinned": false}]}
problemexplains entries that cannot be used as configured (a pin beyond the latest version, a pin to an unknown name); theirversionis 0.GET /api/v1/projects/{project}/skill-pins→200The project’s pins,
{"items": [{"name": "gitflow", "version": 0, "disabled": true}]}. Unlike the effective skills, this includes disabled skills, so a client can show and re-enable them. Requiresskill.readon the project.PUT /api/v1/projects/{project}/skills/{name}/pin—{"version"?, "disabled"?}→204version0 (default) follows the latest published version;Nfixes version N;disabled: trueexcludes the skill from the project. Requiresskill.writeon the project.
DELETE /api/v1/projects/{project}/skills/{name}/pin → 204
Search¶
GET /api/v1/search?q=…→200Hybrid full-text and semantic search over milestones, epics, tickets (title, description, acceptance criteria) and skills (name, description, body). Results are filtered by permission: items need
tracker.readon their project, skillsskill.readon their scope. Filters:kind(itemorskill),project(its items plus the skills of its platform → organization → project chain),limit(default 20, max 50).{"items": [{"kind": "item", "ref": "WEB-1", "title": "Rate limit the public API", "snippet": "429 after 100 requests per minute", "organization": "acme", "project": "WEB", "score": 0.0328}]}
The index follows Core’s event log and is updated within a few seconds of a change. Core currently embeds with the built-in
hash-256model; gateway embeddings for Core content are a follow-up.
Organizations¶
Representation:
{"id": "0199…", "key": "acme", "name": "Acme", "feature_policy": "direct",
"created_at": "2026-10-01T03:40:00Z", "updated_at": "2026-10-01T03:40:00Z", "version": 1}
feature_policy says how the organization’s agents may change features
(Who may change features): direct (default), proposal
or read_only.
POST /api/v1/organizations—{"key", "name"}→201Key: 2–32 lowercase letters, digits, single hyphens; starts with a letter; immutable. Platform-level permission.
GET /api/v1/organizations → 200 list
GET /api/v1/organizations/{organization} → 200
PATCH /api/v1/organizations/{organization}—{"name"?, "feature_policy"?, "version"}→200Changes what is given; at least one of
nameandfeature_policy.
Projects¶
Representation:
{"id": "0199…", "key": "ACME", "organization": "acme", "name": "Acme Shop",
"description": "Online shop", "created_at": "…", "updated_at": "…", "version": 1}
POST /api/v1/organizations/{organization}/projects—{"key", "name", "description"}→201Key: 2–10 uppercase letters and digits, starts with a letter, globally unique, immutable; prefix of ticket keys (
ACME-42).
GET /api/v1/organizations/{organization}/projects → 200 list
GET /api/v1/projects/{project} → 200
PATCH /api/v1/projects/{project} — {"name", "description", "version"} → 200
Milestones, epics and tickets¶
All three are items sharing one per-project key sequence
(WEB-1, WEB-2, …). Representation:
{"id": "0199…", "key": "WEB-2", "project": "WEB", "kind": "ticket",
"title": "Export invoices", "description": "", "state": "backlog",
"type": "feature", "acceptance_criteria": ["CSV has one row per invoice"],
"policy": {"review_mode": "agent", "merge_mode": "auto"},
"epic": "WEB-1", "milestone": "WEB-3",
"created_at": "…", "updated_at": "…", "version": 1}
stage appears while a ticket is in_progress. type,
acceptance_criteria and policy exist only on tickets; epic
only on tickets; milestone on tickets and epics.
Field |
Values |
|---|---|
|
|
|
|
|
|
|
|
Permissions: tracker.read to read, tracker.write to create,
update and transition (Security).
POST /api/v1/projects/{project}/items→201Body:
kind,title(required),description, and for ticketstype,acceptance_criteria,policy,epic;milestonefor tickets and epics. Related epic/milestone must be of that kind in the same project.GET /api/v1/projects/{project}/items→200listQuery filters:
kind,state,epic,milestone. Ordered by key number.
GET /api/v1/items/{item} → 200
PATCH /api/v1/items/{item}→200Body:
versionplus any oftitle,description,type,acceptance_criteria,policy,epic,milestone. An empty string forepic/milestoneremoves the relation.POST /api/v1/items/{item}/transition—{"state", "version"}→200Moves the item on behalf of a human. Allowed transitions:
From (ticket)
To
backlogready,cancelled,donereadybacklog,paused,cancelled,donein_progress,waiting_for_answerpaused,cancelledpausedready,backlog,cancelled,donedone,cancelledbacklogMilestones and epics:
open↔done/cancelled.in_progressandwaiting_for_answerare set only by the orchestrator. Other transitions return400.GET /api/v1/items/{item}/history→200listThe item’s events, oldest first:
{"seq", "type", "occurred_at", "actor", "payload"}.
Dependencies¶
Edges between two items of the same project
(Scheduling). blocks edges must form an acyclic
graph; relates is undirected and informational. Seen from an item,
an edge has a direction:
|
Meaning (from |
|---|---|
|
|
|
|
|
Related, no ordering |
Representation: {"id", "type", "item": {"key", "kind", "title", "state"}}.
POST /api/v1/items/{item}/dependencies—{"type", "item"}→201400if the edge would create a cycle, connects an item to itself or crosses projects;409if it already exists.
GET /api/v1/items/{item}/dependencies → 200 list
DELETE /api/v1/dependencies/{id} → 204
GET /api/v1/projects/{project}/runnable→200list of itemsTickets in state
readywhose blockers are all resolved (doneorcancelled) — what the scheduler may start.
Features¶
The organization’s feature map (Feature map). Features
are addressed by their key, F-<n>, unique within the organization.
Reading needs tracker.read on the organization; callers who can read
only some projects see the features of those projects (and links between
them). Writing needs tracker.write on the organization, or on every
project of the feature. Every change appends a revision.
Feature representation:
{"key": "F-3", "organization": "acme", "title": "Invoice export",
"description": "Exports invoices as CSV and PDF.", "status": "live",
"projects": ["API", "WEB"], "created_at": "…", "updated_at": "…",
"version": 4}
status is planned, in_progress, live, changing,
deprecated or removed; version is the number of the latest
revision.
GET /api/v1/organizations/{organization}/features[?project=&status=] → 200 list
POST /api/v1/organizations/{organization}/features→201{"title", "description"?, "status"?, "projects"?, "reason"?}; the status defaults toplanned.reasonis recorded on the first revision.GET /api/v1/organizations/{organization}/features/{feature}→200The feature with
links: its current links from and to features the caller can see.PATCH /api/v1/organizations/{organization}/features/{feature}→200{"version", "title"?, "description"?, "status"?, "projects"?, "reason"?}.400when nothing changes;409on a staleversion.GET /api/v1/organizations/{organization}/features/{feature}/revisions→200listNewest first:
{"feature", "number", "title", "description", "status", "projects", "author", "reason"?, "cause_kind"?, "cause_ref"?, "review"?, "reviewed_by"?, "reviewed_at"?, "created_at"}— the full state after each change.authoris an actor (human,servicefor agents and the planner);reviewispending,confirmedorrevertedfor revisions that need review.GET /api/v1/organizations/{organization}/feature-reviews→200listRevisions waiting for review, oldest first: each revision with
feature_titleandprevious(the revision before it, absent for a creation) to compare against.POST /api/v1/organizations/{organization}/features/{feature}/revisions/{revision}/review→200{"action": "confirm"|"revert", "comment"?}. Humans only.revertappends a revision restoring the state before (a reverted creation sets the status toremoved), with the reason “Reverted revision n: comment”; only the latest revision can be reverted (409otherwise).409when the revision does not wait for review.POST /api/v1/organizations/{organization}/feature-links→201{"from", "to", "type"}, read “fromtypeto”:derived_from,split_from,merged_into,supersedes,depends_onorrelates. Returns{"id", "from", "to", "type", "created_by", "created_at"}.400for a link to the feature itself or one that exists already.DELETE /api/v1/organizations/{organization}/feature-links/{link}→204The link ends now; it stays in the map’s history.
GET /api/v1/organizations/{organization}/feature-graph[?at=&project=]→200The map as it was at
at(RFC 3339; default now):{"at", "features": [{"key", "title", "status", "projects", "version", "created_at", "updated_at"}], "links": [...], "changes": [{"at", "feature", "kind", "summary"}]}. Each feature appears in its latest revision at that time;linksare those valid then;changeslists every revision and every link added (linked) or removed (unlinked), oldest first, for a time slider. Withproject, only features of that project at that time.
Runs¶
Agent sessions executed by agents (Agents).
Representation: {"id", "project", "ticket", "stage", "status", "spec",
"branch"?, "adapter"?, "result"?: {"summary", "turns", "cost_usd", "parked"?, "session_id"?},
"agent"?, "exit_code"?, "error"?, "created_by", "created_at",
"started_at"?, "finished_at"?, "version"}.
POST /api/v1/items/{item}/runs—{"stage", "spec": {"command", "env"?, "image"?, "workdir"?, "timeout_seconds"?, "files"?}}or{"stage", "agent": {"adapter": "claude-code"|"opencode", "prompt", "timeout_seconds"?}}→201Queues a run of a ticket by hand: a command (
spec) or a coding agent session (agent, Agent runs). Needsrun.manageon the project (platform and organization admins).400for non-tickets, both or neither ofspecandagent, an unknown adapter, an empty command or prompt, a stage not matching[a-z][a-z0-9_-]{0,31}or invalid environment variable names.
GET /api/v1/items/{item}/runs → 200 list, oldest first (tracker.read)
GET /api/v1/runs/{run} → 200
GET /api/v1/runs/{run}/logs?after=<seq>&limit=<n>→200{"items": [{"seq", "stream", "text", "at"}]}: output afterseq(default 0), at mostlimit(default and maximum 1000) chunks.streamisstdout,stderr,system(the agent’s messages) orevent(a session’s events, Agent runs). Poll with the lastseqto follow a running run.GET /api/v1/items/{item}/reports→200What the ticket’s runs reported through the tracker MCP (Tracker MCP), oldest first:
{"id", "run", "kind": "progress"|"stage_report"|"assumption", "outcome"?, "text", "detail"?, "created_at"}; assumptions also carry their review (below). Needstracker.read.GET /api/v1/projects/{project}/assumptions[?review=open|confirmed|rejected]→200The project’s assumption register, newest first (The assumption register): reports of kind
assumptionwith"ticket", "ticket_title", "review"?: "confirmed"|"rejected", "review_comment"?, "reviewed_by"?, "reviewed_at"?, "follow_up"?.openlists unreviewed ones. Needstracker.read.POST /api/v1/assumptions/{report}/confirmand.../reject—{"comment"?}→200the assumptionReviews an assumption; a rejection needs a
commentand creates follow-up work, referenced byfollow_upasquestion:<id>orchangeset:<id>. Needstracker.write;400without a comment to reject,404for a report that is not an assumption,409when it is reviewed already.GET /api/v1/items/{item}/questions→200Questions raised on the ticket — by its runs, or by Ballet about the pipeline (no
run):{"id", "ticket", "run"?, "text", "context"?, "blocking", "status": "open"|"answered", "route"?: "planner"|"human", "answer"?, "answered_by"?, "created_at", "answered_at"?}.answered_byisplanneror the human’s subject.GET /api/v1/inbox→200Open questions of every project the caller can read, in inbox order (Questions and Human Involvement): the question fields plus
"organization", "project", "ticket_title", "ticket_state", "blocked_behind"(unresolved items waiting behind the ticket) and"chat"?(its sub-chat).POST /api/v1/questions/{question}/chat→200a planner sessionThe question’s sub-chat, started on first use (
"question"is set on it). Talk to it withplanner.sendlike any planner session. Needstracker.write.POST /api/v1/questions/{question}/answer—{"answer"}→200the questionAnswers an open question (see Asking a question). Needs
tracker.write;400for an empty answer,409when it is answered already. A blocking question’s answer resumes the ticket’s pipeline once no blocking question is open.POST /api/v1/runs/{run}/input—{"kind": "message"|"interrupt", "text"?}→204Sends a human’s input to the run’s running coding-agent session (Talking to a running session): a message (
textrequired, at most 20 000 characters) is delivered when the current turn ends; an interrupt stops the turn first, then deliverstext, if any.400for a plain-command run,409when the run is not running or its session has ended. Needsrun.manage.POST /api/v1/runs/{run}/cancel→200Cancels a queued run at once; an active run is cancelled on its agent (its status changes when the agent reports).
409if the run has ended. Needsrun.manage.
Digest¶
GET /api/v1/projects/{project}/digest[?since=…&until=…]→200What happened in the project from
since(RFC 3339; default 24 hours beforeuntil) tountil(default now) (Observability):{"project", "since", "until", "done", "failed", "started", "merged", "waiting"(tickets:{"key", "title"?, "at", "detail"?})``, “questions_raised”, “answered_by_planner”, “answered_by_human”, “open_questions”, “assumptions”, “proposals”, “runs”`` (by status)``, “tokens”, “interventions”, “markdown”}``. Needstracker.read;400whensinceis not beforeuntil.
Budgets¶
See Budgets.
GET /api/v1/projects/{project}/budget→200{"ticket_tokens", "daily_tokens", "used_today", "updated_by"?, "updated_at"?, "version"}— limits (0: none) and the counted tokens used today;version0 when never set. Needstracker.read.PUT /api/v1/projects/{project}/budget—{"ticket_tokens", "daily_tokens", "version"}→200Sets the limits if the budget is at
version. Needsproject.update;400for negative limits,409when it changed meanwhile.GETandPUT /api/v1/organizations/{organization}/budgetThe same for an organization (
organization.read/organization.update); itsticket_tokensapplies to projects without their own.
Pause and kill switch¶
See Pause, resume and the kill switch.
GET /api/v1/pauses→200{"items": [{"scope": "platform"|"project", "project"?, "reason"?, "paused_by", "paused_at"}]}— the platform pause and the pauses of projects the caller can read.PUT /api/v1/projects/{project}/pause—{"reason"?}→200the pausePauses the project’s autonomous work. Needs
run.manageon the project.DELETE /api/v1/projects/{project}/pause→204Resumes it;
404when it is not paused.POST /api/v1/projects/{project}/kill—{"reason"?}→200{"pause", "cancelled"}Pauses the project and cancels its runs in progress;
cancelledcounts them.PUT /api/v1/pause,DELETE /api/v1/pause,POST /api/v1/killThe same for the whole platform; need
run.manageat platform scope.
Execution settings¶
GET /api/v1/projects/{project}/execution→200{"project", "repo_url", "default_branch", "image", "setup", "env", "branch_template", "git_name", "git_email", "updated_at"?, "version"};version0 when never set. Needstracker.read.PUT /api/v1/projects/{project}/execution— the same fields and theversionread →200Replaces the settings (Workspace preparation). Needs
project.update.400for arepo_urlthat is not an https, ssh,git@host:pathor file URL or that contains credentials, abranch_templatewithout{ticket}or producing an invalid branch name, multi-line or empty setup commands, orenvnames that are invalid or start withBALLET_.409on a staleversion.
The repository token is a credential with provider git
(Credentials).
Three more fields choose how Ballet follows the project’s branches
(ADR-0007: Code Review on Git Platforms via Forge Adapters):
forge — github, git (plain git hosting, no pull requests) or
"" (GitHub for github.com repositories, else plain git);
forge_api_url — the GitHub Enterprise API URL (default
https://api.github.com); link_template — for plain git, the link of
a branch with {branch} and {base}.
answer_window_minutes (0–1440, 0: Core’s agents.answer_window)
is how long a session waits online for answers to its blocking questions
before it parks (Waiting for answers, parking, resuming).
pool names the agent pool the project’s runs execute on: agents
labelled pool=<pool> (Build an agent pool from a devcontainer); empty: any agent.
image is kept but not used by agents.
feature_policy overrides the organization’s feature policy for this
project’s agents (direct, proposal, read_only); empty:
inherit.
Pull requests¶
A ticket’s branch goes into the default branch through a pull request on
the project’s forge; review happens there. Representation:
{"ticket", "forge", "number", "url", "title", "head", "base",
"head_sha"?, "state": "open"|"closed"|"merged", "draft", "mergeable"?,
"checks": "none"|"pending"|"success"|"failure", "review":
"none"|"approved"|"changes_requested"|"commented", "updated_at"}.
GitHub: Core opens the pull request with the project’s git token.
checkscombines the head commit’s check runs and commit statuses;reviewis each reviewer’s latest verdict (any “changes requested” wins). Merging squash-merges.Plain git: there is no pull request (
number0). The branch isopenonce pushed andmergedonce the default branch contains its last commit; squash merges are not detected.urlcomes fromlink_template. It cannot be merged through Ballet.
Core refreshes open pull requests every forge.poll_interval and records
item.pr_updated events on the ticket when they change.
GET /api/v1/items/{item}/pull-request → 200 as last seen; 404 when none (tracker.read)
POST /api/v1/items/{item}/pull-request→200Opens the pull request of the ticket branch (named by the project’s branch template) into the default branch, or finds the existing one.
400when the project has no repository;502when the forge fails. Needstracker.write.POST /api/v1/items/{item}/pull-request/refresh→200Reads the current state from the forge (
tracker.read).POST /api/v1/items/{item}/pull-request/merge→200Squash-merges an open pull request. Needs
tracker.writeand a human caller (merging by policy comes with the orchestrator).409if it is not open;400on plain git.
Planner sessions¶
Chats with the planner agent; messages are sent and streamed over the realtime API (Planner chat).
POST /api/v1/projects/{project}/planner/sessions—{"title"}→201Needs
tracker.write. Returns{"id", "project", "title", "created_by", "created_at", "updated_at", "running"}.
GET /api/v1/projects/{project}/planner/sessions → 200 list, most recently active first
GET /api/v1/planner/sessions/{session}→200The session with its transcript,
"messages": [{"seq", "role", "content", "author"?, "stop_reason"?, "usage", "created_at"}].contentis a list of blocks:{"type": "text", "text"},{"type": "tool_use", "tool_use_id", "name", "input"}(assistant) and{"type": "tool_result", "tool_use_id", "text", "is_error"?}(roleuser, written by the planner).usageholds the token counts of assistant messages.
Plan changesets¶
A plan changeset is a batch of planning changes proposed by the
planner (or a person) and decided by a human, wholly or operation by
operation (Workflow). Proposing needs tracker.write
on the project; applying and rejecting additionally need a human
caller, so service identities such as the planner can propose but never
approve.
An operation is one of:
{"kind": "create_item", "ref": "login",
"create": {"kind": "ticket", "title": "Login", "epic": "$auth",
"acceptance_criteria": ["OIDC sign-in works"]}}
{"kind": "update_item",
"update": {"item": "WEB-7", "title": "Profile page (needs login)"}}
{"kind": "add_dependency",
"dependency": {"from": "$login", "to": "WEB-7", "type": "blocks"}}
Item references (epic, milestone, from, to) are either
the key of an existing item of the project or $ followed by the
ref of an earlier create_item operation in the same
changeset. create takes the fields of a new item
(Milestones, epics and tickets); update changes only the fields it
lists ("" for epic or milestone removes the relation).
POST /api/v1/projects/{project}/changesets—{"title", "summary"?, "operations"}→201Validates every operation against the current project as if all were approved (fields, references, relation kinds, no duplicate or cyclic dependencies, at most one update per item, at most 200 operations) and stores the changeset with status
proposed. Nothing else changes.400with the failing operation’s number otherwise.
GET /api/v1/projects/{project}/changesets[?status=proposed|applied|rejected] → 200 list, newest first
GET /api/v1/changesets/{changeset}→200{"id", "project", "title", "summary", "status", "operations", "proposed_by", "created_at", "decided_by"?, "decided_at"?, "approved", "results", "version"}. Once applied,approvedlists the applied operations (0-based) andresultshas one entry per operation:{"key"}for created or updated items,{"dependency"}for added dependencies,{}for operations that were not approved.POST /api/v1/changesets/{changeset}/apply—{"operations": [0, 1, 4]}→200Applies exactly the listed operations in one atomic write and marks the changeset
applied. Every operation an approved one refers to through$refmust be approved too (400otherwise). Operations are validated again against the current project; keys of created items are assigned now.409if the changeset was already decided.POST /api/v1/changesets/{changeset}/reject→200Marks a proposed changeset
rejected;409if already decided.
Each decision records a changeset.applied or changeset.rejected
event; created items, updates and dependencies record their usual
events, with the changeset ID in the payload.
Every create and update records one event (Data).