for agents: this page is markdown at https://switch-yard.dev/docs/api.md; every page is listed in llms.txt.
REST API
Every project answers under https://app.switch-yard.dev/v1/projects/<project>; the paths below are relative to it. Send your key as a header, Authorization: Bearer <key>: an admin key for the project, a task key for one task, an agent group key for many agents at once, or a signed-in person's session from the app. Answers are JSON and carry seq, the position in the project's history; errors carry { "error": { "code", "message", "hint" } } as the errors page lists. A 503 with a Retry-After header means wait that long and send the same request again; nothing you sent is lost.
Paths say workspace where the product says task; the ids are the same (W8, 8 or task 8 are all accepted wherever a task is named). This page is rendered from the design document's route table, so it changes when a route does.
| Method | Path | What it does | ||
|---|---|---|---|---|
| GET | (the project itself) | { seq, project }: its id, name, branch, main, remote and, when it follows one, its GitHub repository; POST /v1/projects answers the same project with the admin key | ||
| GET | /me | Which principal the key or session is: { kind, workspace?, user?, fleet? } (kind is fleet for an agent group key, or workspace with fleet for the MCP session of one that claimed a task) | ||
| POST | /admin-key/rotate | Issue a new admin key for the project (project admin or the operator); every earlier admin key stops at once, with no grace period, since a rotation is usually for a leaked key. Answers { seq, adminToken, revoked }, with the key shown once, and records project.admin_key (the count only, never the key), which cannot be undone; the hosted product also writes it to the organization's audit log. The board's settings page has the button and keeps itself signed in with the new key. A holder of a leaked admin key can rotate it too and lock the owners out for a moment; an owner rotates again from a signed-in session, and the audit log names who rotated | ||
| POST | /fleets | Issue an agent group key (project admin): { name, tags?, permission?, maxWorkspaces?, ratePerMinute?, expiresInDays? }; answers { seq, fleet, token } with the key, which is shown once, and records fleet.create; fleet carries expiresAt, and once known lastUsedAt, disabledAt, rotatedAt and previousValidUntil | ||
| POST | /fleets/:fleet/rotate | Issue a new secret for the agent group (project admin), { graceMinutes?, expiresInDays? }: the old secret works for graceMinutes (60, at most 10,080); answers { seq, fleet, token } and records fleet.rotate, which cannot be undone | ||
| POST | /fleets/:fleet/disable, /fleets/:fleet/enable | Stop or restart an agent group key and its tasks' keys without revoking it (project admin); answers { seq, fleet } and records fleet.disable or fleet.enable | ||
| GET | /fleets | The project's agent group keys (project admin), each with the tasks it created and how many are active | ||
| POST | /fleets/:fleet/revoke | Revoke an agent group key (project admin), { abandon? }: records fleet.revoke once, and with abandon abandons the agent group's active tasks; answers { seq, fleet, abandoned } | ||
| POST | /workspaces/claim | The MCP session of an agent group key claims its task: { task, plan?, tags?, agent?, model?, resume? }; answers { seq, claimed, workspace, credentials, resume } without a task key | ||
| POST | /workspaces | Create a task (project admin, or an agent group key), with optional tags and permission (the project's taskPermission when absent, submit unless changed); returns remote URL, key, permission; 409 while main has no commits yet or the host's default branch is not the project's. The copy of the repository and its key are made outside the coordinator's lock, and the task starts at the commit the copy of the repository copied. The same requestId sent again within a day returns the task it created, with fresh credentials, instead of a second task (409 if the request differs, tags included, or that task has finished) | ||
| POST | /workspaces/:workspace/tags | { set?, remove? }: merge tags into the task's and drop the keys named; answers { seq, workspace, tags }. Records workspace.tag unless nothing changes; 400 past 16 tags or 1,024 characters, or for a key both set and removed | ||
| PUT | /workspaces/:workspace/plan | Set or update the plan; returns the task's intent warnings once the judge has answered about the new plan (or the comparison fell back to wording), waiting for it outside the coordinator's lock | ||
| GET | /status?min_seq=&workspace=&tag= | The ProjectState: every active task and the 20 most recently finished, with olderFinished when there are more; the inbox lists active tasks only. With workspace, the same shape for one task: the task, the tasks it is stacked on and every active task built on it, with the tasks in between (finished ones included), the warnings involving it and its inbox items. With tag=key:value (repeated, ANDed; not with workspace), the same shape for the tasks carrying those tags: the tasks, the warnings involving any of them and their inbox items, with the whole merge queue. The MCP yard_status without a task lists a line per task and the warnings grouped by kind and tasks (MCP tools) | ||
| GET | /workspaces?status=&before=&limit=&tag= | status=active (the default): every active task. status=finished: finished tasks older than the cursor, newest first, up to 100, with the next cursor. Both answer { seq, workspaces } and take the status's tag filter | ||
| GET | /workspaces/:workspace/warnings | Warnings involving a task, with suggestions | ||
| GET | /workspaces/:workspace/notice | Switchyard's note on a task, the one its pushes print as remote: switchyard: lines and every MCP reply to its key carries as notice: what Switchyard last changed on it and how to catch up, its merge queue state or why it was sent back, its conflicted files and up to two warnings; absent when there is nothing to say | ||
| POST | /workspaces/:workspace/claims | Claim paths or symbols; returns the task's intent warnings like PUT /plan | ||
| POST | /workspaces/:workspace/observe | Read the task's branch now, after a push, instead of waiting for the push event (which stays as the backstop); a merged or abandoned task answers 409, since its pushes are not tracked | ||
| POST | /workspaces/:workspace/ops | Run an engine operation (sync, absorb, split, …); with expectHead, only if the task's branch is still at that commit (409 otherwise), after catching up with a push the coordinator has not seen yet | ||
| POST | /workspaces/:workspace/credentials | Mint a fresh Git key for the task remote; 409 once the task has merged or been abandoned | ||
| POST | /main/credentials | Mint a read-only Git key for main (one hour), for anyone in the project, a merged task's read-only key included | ||
| POST | /workspaces/:workspace/submit | Freeze the task at its head and enqueue it on the merge queue (refused with nothing to merge, unresolved conflicts or an abandoned parent); expectHead names the head the caller pushed, and a task elsewhere is refused with 409 after the coordinator catches up with pushes it had not seen; the key of a task with commit permission gets 403 | ||
| GET | /changes/:change?version= | A change at its latest version, or the one version names: ChangeDetail with its task, subject and commit, every version recorded for the Change-Id (number, commit, task, when it was recorded, subject, and unavailable once its commit is known to be gone), the diff against its parent (files, base, truncated) and the reason behind it (agent, task, and the task plan's summary as plan). A version whose commit is gone answers with no files and unavailable saying why; an unknown change or version answers 404 naming the change; a version that is not a positive integer 400. Readable like /status | ||
| GET | /changes/:change/interdiff?from=&to= | What changed between two versions: { seq, change, from, to, rebased, truncated, files }. 400 unless from and to are two different version numbers; 404 for an unknown change or version, or one whose commit is gone, naming it. Readable like /status | ||
| GET | /conflicts | { seq, conflicts }: a ConflictView per conflicted path in each active task's stack, and the ones fixed or resolved within the last 50 operations. Readable like /status | ||
| POST | /conflicts/:change/resolve | Climb the conflict steps; expectHead leases it like a task operation, and the answer names the task's new head. In a task with two commits carrying the Change-Id it resolves the conflicted one, and answers 409 when both are conflicted. { "rung": "agent" } accepts the resolver step's proposals for the change's current version: the task's own key or a person (an admin or member) may, an agent group key that has not claimed the branch gets 403, and a version with no proposal answers 409. A version an agent accepted waits for a person's approval in the merge queue like a recipe fix (unless fixes is auto); a person's accept is the approval. { "rung": "human", "files": [...] } is refused with 400 invalid when a file still holds a full conflict block in order (<<<<<<<, =======, >>>>>>>, each alone on its line or, for the outer two, followed by a space and a name), naming the file and line, hint "remove them and send it again" | ||
| POST | /review/:change/:version | Approve, comment or request a fix; the version names one commit, even in a task with two commits carrying the Change-Id. The project admin, the operator and members of the project's team review; a task key gets 403, its own changes included. The approver's teams are recorded with the approval; when the task's facts are known and touch owned paths, an approval answers with owners (each team, its paths, the approvals it needs and has, and whether this approval counted) and, while a team is still short, a note saying why | ||
| GET | /why?path=&line=&workspace= | Change, task, agent and plan that last wrote a line: a task key blames its own task head, an admin the live integration or the task it names | ||
| POST | /live | A single-use ticket (60 s) for a live WebSocket connection | ||
| GET | /oplog?since= or /oplog?last= | Read the log: the actions after since, or from the newest last on (the board's first read, sent before the status has said how long the log is; last wins when both are given); at most 500 a page, with seq the newest | ||
| GET | /attestations/:commit | The attestation of a merged commit (7 or more hex digits): the signed text, signature, signer, whether it verifies and whether its note is on main | ||
| GET | /signer | The project's signing key and its allowed_signers line | ||
| GET | /insights?days= | Insights over the last 1, 7 or 30 days (default 7; another value 400), computed from the operation log (project admin or a member of its team): tasks started, submitted, sent back and merged, merges, approvals, submit to merge, start to merge and the wait for a review (median and the slowest tenth), the typical change size, counts per day, and per agent and model the same counts, send-back rate, conflict rate and submit to merge | ||
| GET | /settings | Everything the project is set to, for the settings page (project admin or a member of its team; tasks get 403): the project (name, main branch, storage), the GitHub connection when it follows a repository (the app's granted permissions and events against what Switchyard needs, each gap with what it unlocks, links to fix it on GitHub, and when the last push and pull request event arrived), the rules file on main as read at which commit (found, missing or unreadable), the check network, and the values fixed for now. When the Git engine does not answer the rules read in 10 s, the page still answers: with the last rules read and rules.unread "…, so these are the rules as of main @ abc1234; reload to read the newest", or with empty lists and "…, so the rules on main are not read yet; reload to try again" | ||
| PUT | /pause | Pause merging, { "reason": "incident 42", "until"?: "2026-10-16T09:00:00Z" } (project admin; tasks get 403; an empty reason, an unreadable time, a time past or more than 30 days ahead get 400). Answers { seq, paused }. Records project.pause, which undo reverts, and wakes the merge queue | ||
| DELETE | /pause | Resume merging (project admin). Answers { seq, paused: null }; nothing is recorded when merging is not paused | ||
| PUT | /fixes | Whether fixes Switchyard makes wait for a person, `{ "fixes": "approve" \ | "auto" } (project admin; tasks get 403, another value 400; default approve). With approve the merge queue holds a change version the recipe step wrote until that version is approved; auto lets it merge once its checks pass. POST /v1/projects takes fixes; settings report it. Records project.fixes`, which undo reverts, and wakes the merge queue | |
| PUT | /task-permission | Who submits a finished task, `{ "taskPermission": "submit" \ | "commit" } (project admin; tasks get 403, another value 400; default submit). It is the permission a new task gets when its request names none: submit lets the agent submit its task when the work is done, commit leaves submitting to a person. Tasks made earlier and agent group keys keep their own permission. POST /v1/projects takes taskPermission; settings report it. Records project.task_permission`, which undo reverts | |
| PUT | /conflict-data | Whether the project's conflict records (the conflicting lines, the fix and whether a person accepted it) may help improve Switchyard's resolver, `{ "conflictData": "private" \ | "share" } (project admin; default private). Nothing uses shared records yet; the setting records the project's choice before anything could. Records project.conflict_data`, which undo reverts | |
| GET | /webhooks | The project's webhook endpoints (project admin; others 403): id, url, events, failing (attempts failed since the last success), dropped (events given up on), lastSuccessAt, lastError, lastDroppedAt; never the secret | ||
| POST | /webhooks | Add an endpoint, { "url": "https://…", "events"?: [...] } (project admin): https on a public host, no IP literal, no local name, no user info, not Switchyard's own host; at most 5 endpoints. Answers the endpoint with its signing secret, shown only here. Records project.webhooks with the URL without its query string, never the secret; it cannot be undone | ||
| DELETE | /webhooks/:id | Remove an endpoint and every event still waiting for it (project admin) | ||
| PUT | /rules | Keep the rules and checks in Switchyard, { "rules": "<the text of a rules file>" }, or "" to stop (project admin; tasks get 403, text that does not parse 400, over 64 KiB 400). While main has no .yard/rules.toml, the merge queue, checks and settings read these instead; a file on main always wins. POST /v1/projects takes the same text as rules. Records project.rules, which undo reverts, and wakes the merge queue; answers { seq, source } with source switchyard or none, and settings report rules.source (repository, switchyard or none) and the kept text as rules.stored | ||
| PUT | /reviews | Turn reviews on or off, `{ "reviews": "on" \ | "off" } (project admin; tasks get 403, another value 400). Off adds a last rule that lets every change no rule covers merge once its checks pass, so rules that ask for review still apply; records project.reviews`, which undo reverts, and wakes the merge queue so waiting work is decided again | |
| PUT | /checks/network | Set the network the project's checks may reach, `{ "network": "open" \ | "registries" \ | "off" } (project admin; tasks get 403, another value 400); records project.check_network unless it is already set, and answers { seq, network }` |
| POST | /oplog/:seq/undo | Undo an operation; operations on a merged task are final (409), the kinds the design document names cannot be undone (400), and an operation already undone, even by an undo running at the same time, answers 409, as does a integration rebuild whose integration never reached main |