nextv2.0.0
HTTP API

Repos, chains, harnesses and the server

The board's routes for connecting a repo, reading chain templates and harnesses, applying saved changes, checking for updates, disk use and opening editors.

This page lists the board's routes for connecting, changing and disconnecting a repo, chains and the library, harnesses, changes saved but not applied, the update check and editors. Like the other board routes they can change in any release: see Filing, reading and changing work items for the rule, and the stable routes for the two that keep their shape. Prefer the kraft command with --json.

RouteWhat it does
GET /api/reposLists the connected repos and workspaces.
POST /api/repos/probeReads a repository and proposes how to connect it.
POST /api/reposConnects a repository.
PATCH /api/repos?path=Changes a connected repo's entry in repos.yaml.
DELETE /api/repos?path=Disconnects a repo.
GET /api/templates/chainsLists every chain.
GET /api/templates/chains/{id}/resolvedAnswers one chain with its extends expanded.
GET /api/templates/libraryAnswers library.yaml and every component in it.
GET /api/templates/library/{ref}Answers one library component.
GET /api/templates/steering/previewShows what an agent task reads at launch.
GET /api/harnesses/profilesAnswers the harness profiles and the agent profiles.
GET /api/harnessesLists each provider and whether its CLI is installed.
GET /api/harnesses/providersAnswers each provider's capabilities.
GET /api/applyLists what is saved and not running yet.
POST /api/apply/reloadRereads the library, policy.yaml and intake.yaml.
POST /api/templates/reloadRereads the library, policy.yaml and intake.yaml, and answers what it refused.
POST /api/apply/restartRestarts a server that a service manager started.
GET /api/updateAnswers the cached update check.
POST /api/update/checkAsks the release feed now.
GET /api/storageAnswers the disk the worktrees use, item by item.
POST /api/storage/previewShows what archiving some items would free and lose.
GET /api/editorsLists the editors this machine can open.
POST /api/documents/{id}/openOpens a document in an editor.
POST /api/work-items/{id}/artifact/openOpens the pending gate's document.
POST /api/work-items/{id}/artifacts/{kind}/openOpens a task's document.
POST /api/work-items/{id}/open-worktreeOpens the item's worktree.

Connecting a repo

GET /api/repos

Answers the connected repos and workspaces, and the checkout the server was started in when it is not connected yet.

FieldTypeMeaning
reposlistOne entry per connected repo, as repos.yaml holds it, defaults included.
workspacesobjectThe workspaces: section, by workspace id.
suggestedstring or nullThe git checkout the server was started in, when it is one and not connected yet, else null. The first-run board offers it as the path to connect.

POST /api/repos/probe

Reads a repository and changes nothing. The proposal answer says what it returns.

FieldTypeDefaultMeaning
pathstringrequiredThe absolute path of the repository.
detectbooleantrueWith false the route answers only what needs no detector table: the resolved path, name, branch, submodules, beads and forge.
test_commandstringnoneThe root's test command, in place of the proposed one. "" declares that the repo has no tests.

With test_command the answer is the proposal that connecting with that command would save. The probe runs in a process of its own with a two-minute limit, so a repository's files cannot stall the server. How connect proposes commands says how a proposal is made, what limits the probe, and what connect does when the probe fails. The 429 below is only on this page.

StatusWhen
400The path is not a directory or not a git repository, detectors.yaml does not load (the answer names it), or the probe ran past two minutes.
422path is relative.
429detect is true and two probes are already running. Try again shortly. A probe with detect: false is never queued.

POST /api/repos

Connects a repository: it probes it, saves the entry to repos.yaml, and indexes the repo for search. It answers 201 with the saved entry and the proposal answer.

FieldTypeDefaultMeaning
pathstringrequiredThe absolute path of the repository.
namestringthe directory nameThe repo's name.
default_chainstringdefaultThe chain its items run when none is named.
test_commandstringthe probe's proposalThe root's test command. "" declares a repo with no tests, which may be enabled.
setup_commandstringthe probe's proposalThe command that prepares a worktree. "" declares none.
test_scopeslistthe probe's nested scopesTest scopes by directory. An empty list is read as unset.
enabledbooleantrue with a test command or test scopes, else falseWhether new items may target the repo.
forge, project, models, deny_tools, steeringthe probe's forge and project, else emptyThe fields of the same names in repos.yaml.

A test_command or setup_command in the body replaces the proposal. If the body gives both, the route connects a repo the probe fails on, with those commands, and says why in probe_failed.

StatusWhen
400The path is not a git repository. Or the probe fails (detectors.yaml does not load, or it ran past two minutes) and the body does not give both commands.
409The repo is already connected. It is checked before anything else is read.
422The path is relative, the repository has no commit yet, enabled: true has no test command or scopes, or repos.yaml would not load with the entry.

PATCH /api/repos?path=

Changes fields of the connected repo at path, the absolute path of its entry in repos.yaml. The Settings screen calls it. A field the body leaves out keeps its saved value, and one sent as null is cleared. Any save sets managed: true on the entry.

FieldTypeMeaning
name, forge, project, default_chainstringAs in POST /api/repos.
test_command, setup_commandstringThe commands. "" declares none.
test_scopeslistTest scopes by directory.
enabledbooleanWhether new items may target the repo.
models, deny_tools, steeringobject, list, listThe fields of the same names in repos.yaml.
local_files, intent_dirlist, stringThe fields of the same names in repos.yaml.
library_auto_updatebooleanWhether text-only changes to the repo's accepted repo library are taken without a person.

The answer is the whole saved entry, defaults included.

StatusWhen
404path is not a connected repo, or is relative.
422repos.yaml would not load with the change, and nothing is written: an unknown steering name, a glob in local_files, an intent_dir that leaves the repo. The detail says which.
422The change enables a repo that has no test command or test scopes, or clears the last test command of an enabled repo. Disable it, or set test scopes in the same change.

DELETE /api/repos?path=

Disconnects the repo at path: it removes the entry from repos.yaml and from the search index. No file of the repository is touched and no work item is dropped. kraft repo disconnect calls it. It answers 204 with no body.

StatusWhen
404path is not a connected repo, or is relative.
409The repo has open items, paused ones included. The answer gives the count: complete, cancel or abandon them first.
422A workspace in repos.yaml still names the repo.

The proposal answer

POST /api/repos/probe and POST /api/repos answer the proposal beside the repo's facts: test_command, test_scopes, setup_command, and the evidence below. None of the evidence is stored in repos.yaml.

FieldMeaning
candidatesEvery command the evidence supports: {dir, role, command, tier, source, marker, detector, family, corroborated, chosen}. role is test or setup. tier is runner, ci, toolchain or devenv. dir is "" for the root. chosen marks the proposal.
scopesOne {dir, test, setup} per directory the proposal covers, root first, with commands as they run from inside that directory.
missing_setupDirectories with a test command and nothing to prepare them (. for the root), so setup_command is left undeclared.
test_markersThe file each proposed test command came from.
read_fromThe commit read: refs/remotes/origin/HEAD's target, refs/remotes/origin/main or HEAD, or null for a repo with no commit (read from its working copy).
missing_toolsPrograms a proposed command runs that the server's PATH does not have, one {dir, tool} each. A work item fails on them until they are installed.
stoppedWhy no test command was proposed, one {dir, reason, detector} per directory that stopped it (. for the root): a pyproject.toml with no lockfile, or a root project with no test command found beside nested ones. Empty when nothing stopped.
probe_failedOnly on POST /api/repos, when both commands were given and the probe failed: why.

Chain templates

These routes read the chains the Templates editor shows. Each answers 503 when the library or a chain does not load, with template library invalid, refusing work: and the file that broke it.

GET /api/templates/chains

Answers every chain, by id.

FieldTypeMeaning
idstringThe chain id.
descriptionstring or nullAs authored, even when the chain does not resolve.
nodeslistThe resolved nodes, empty when the chain does not resolve. Each carries icon, the node's own icon name, or null when it sets none (always for a gate).
gatesintegerHow many of the nodes are gates.
errorstring or nullWhy the chain does not resolve.
usesobjectPer node id, the ids of the library components that node is built from. A node built from none is left out.
addonobject or null{id, version} of the Kraft add-on the chain comes from, null for one of your own.

Answers 503 as under Chain templates.

GET /api/templates/chains/{id}/resolved

Answers one chain with its extends expanded.

FieldTypeMeaning
idstringThe chain id.
chainobjectThe chain as written plus what it inherited, with no defaults filled in.
task_pathslist of stringsThe canonical path of every task.
steeringobjectThe steering text the chain selects, by profile.
nodeslistThe resolved nodes, as in GET /api/templates/chains.
documentsobjectEach gate id mapped to the list of document kinds the execution nodes before it produce, in chain order: what that gate's artifact can name.

It answers 404 for an unknown chain and 422 for one that does not resolve. Also 503 as under Chain templates.

GET /api/templates/library

Answers {file, text, components}: the path of library.yaml, its text, and every component in it, section by section. See The library component. Also 503 as under Chain templates.

GET /api/templates/library/{ref}

Answers one component, shaped as in the library component, by its id or a bare name no other section shares. It answers 404 when nothing matches, and 409 naming each id when two sections share the bare name. Also 503 as under Chain templates.

GET /api/templates/steering/preview

Answers what the agent task at canonical path task of chain chain reads at launch in the repository whose name or path is repo, section by section in launch order. Nothing launches. The answer is {sections: [{kind, source, text}]}.

QueryMeaning
chainThe chain's id. Required.
taskThe task's canonical path. Required.
repoThe repository's name or path. Required.

A missing parameter answers 422.

kind is one of:

kindsource
contractkraft
documentThe kind it produces.
review_packagekraft
skillThe skill.
intentThe repository's intent_dir.
steeringrepo:<profile> or task:<profile>.
usagekraft. Only when the task's harness reports usage as a result file.

There is one steering section per steering profile: the repository's first, then the task's. Joined, the texts are the launch's context. Per-launch values are placeholders (<work item id>, <session id>, <title>, <task instruction>).

StatusWhen
404The chain, the task or the repository is unknown.
422The chain does not resolve, the task is not an agent task, or its harness, skill or steering cannot resolve.
503repos.yaml does not load, or the library or a chain does not, as under Chain templates.

The library component

GET /api/templates/library and GET /api/templates/library/{ref} return components with these fields:

FieldMeaning
idThe section and the name, tasks.implementer.
kindIts section.
nameIts name within the section.
definitionThe component as written.
issuesThe lint issues that name it.
used_byThe ids of the chains that use it.
used_by_paths[{chain, path, overrides, via?}]: each component of a chain that extends or selects it. See below.
addon{id, version} of the add-on the component comes from, null for one of your own. An add-on's component has a qualified name, and its id is tasks.release:implementer.

In used_by_paths, path is that component's canonical path, overrides is whether it sets any key besides id, extends and kind, and via is the library node it reaches the chain through, when it sits in that node's own definition. For example, {chain: "default", path: "verification.review.code_review", overrides: false, via: "verification"}.

An add-on's chains and components

A write that would save an add-on's chain or component answers 409, with <id> comes from add-on <addon id>; extend it or copy it to your library. That holds for PUT /api/templates/chains/{id}, for every write to a chains draft of that id, and for a draft op that addresses the component. copy_component and new_chain copy one.

Filing an item on a chain whose add-on store is still being restored answers 503 with add-on <id> is being restored; retry.

Harnesses

The web UI edits harnesses through a draft (/api/drafts/harnesses/...). These routes read the published state.

GET /api/harnesses/profiles

Answers {profiles, agent_profiles, file, error}.

  • file is the path of harnesses.yaml, and error is why it does not load, else null.
  • Each entry of profiles is a harness profile: {id, provider, enabled, executable, defaults, used_by, chains}.
  • Each entry of agent_profiles has id, addon, used_by, chains, effort, model (provider to model id), providers (provider to {model, effort}), fallback, problems and problem_details.
  • addon is {id, version} for an agent profile an add-on ships, whose id is qualified (release:careful), and null for one of your own.
  • effort is the one effort every provider shares, or null when they differ: read providers then.
  • problems is the entry's list of messages, and problem_details the same list as {chain, task, profile, provider, field, message}, where field is model, effort, or null for a profile that is not defined.
  • Each fallback entry has its own problems and problem_details.

GET /api/harnesses

Answers one entry per provider: {id, label, executable, executable_found, efforts, models, capabilities}. executable_found is whether the command is on PATH now. There is no version.

GET /api/harnesses/providers

Answers {valid, invalid}: each provider's capability surface by id, and the providers whose files did not load, with why. A provider also lists efforts and models: the plain-word values it limits that option to, [] when it takes any string or a pattern.

Saved but not applied

A restart or a reload applies what you saved. The apply_changed live message says when this list changes.

GET /api/apply

Answers what is saved and not running yet: {restart: [{id, file, text}], reload: [{id, file, text, problem?}], managed}. Both lists are empty when nothing is pending. managed is true when a service manager started this server, the only case POST /api/apply/restart can restart it.

  • A restart item is access.bind or access.port: access.yaml names an address the server is not bound to. It is left out when KRAFT_HOST or KRAFT_PORT is set, since the environment wins and a restart would change nothing.
  • A reload item is disk:<file> for library.yaml, chains/*.yaml, policy.yaml, intake.yaml, addons.yaml or addons.lock whose bytes differ from the ones last loaded. A file rewritten with the same bytes is no change. problem is the first problem found in the file as it is on disk.

POST /api/apply/reload

Rereads the library, policy.yaml and intake.yaml (and replaces the intake poller), and answers the new GET /api/apply. A policy or an intake.yaml that does not load is refused and stays pending, with its problem, and the running one is kept.

POST /api/templates/reload

Does the same as POST /api/apply/reload, and answers {valid, invalid_templates, refused_policy, refused_intake}: the valid chain ids, the chains that do not load, and the last two each the refusal's reason or null. kraft admin reload prints them and exits 1 on either refusal.

The body is optional. {"only": ["addons.yaml", "addons.lock"]} rebuilds the library from the add-on files alone and leaves a pending edit of policy.yaml or intake.yaml pending. only takes those two names and no other. It is what a kraft admin addon verb sends.

POST /api/apply/restart

Restarts the server when a service manager started it (the launchd or systemd unit kraft admin install-service writes). It answers 202 with {"restarting": true} and runs kraft admin restart detached. Nothing restarts the server unless this is called.

The restart ends the agent of every active item, and each stops as failed until it is retried, as kraft admin restart warns: see A restart ends running agents. The web UI counts the active items from GET /api/work-items (the rows whose status is active) and shows the number in its confirmation.

StatusWhen
409The server was started from a terminal. The answer is {"detail": "started from a terminal: restart it there"}.
500The restart could not start.

Update check

GET /api/update

Answers {installed, latest, channel, python, behind, checked_at} from the daily cached check. python is the interpreter the server runs on, as 3.14. ?channel= is one of stable, rc, beta and alpha (400 otherwise) and defaults to the installed version's own. It is not stored.

When the feed cannot be reached, latest and behind are null: unknown, not "up to date". checked_at is the time of the last check that succeeded on that channel (null when there has been none), so it can be old while latest is null. Installing the update is kraft admin update; the server never does it.

POST /api/update/check

Skips the cache, asks the release feed now, and answers the same shape as GET /api/update. It takes the same ?channel=.

Editors

The routes that open a file in an editor start a process on the machine that hosts the server. They are loopback only: any other caller gets 403.

GET /api/editors

Answers {available, system, default}.

  • available lists the editors (code, cursor, zed, obsidian) whose executable this server's machine has on its PATH, or, on macOS, whose app is installed in /Applications or ~/Applications (it is then opened with open -a).
  • system says whether the system opener (open, xdg-open) is there.
  • default is editor from theme.yaml, else KRAFT_EDITOR, else null.

POST /api/documents/{id}/open

Opens an indexed document, by its absolute path, in an editor.

FieldTypeDefaultMeaning
editorstring or nullnullA name from available, "system" for the system opener, or null for the default above, else the system opener.

The answer is {document_id, path, editor}.

StatusWhen
400editor is not a known editor name, or the document's path leaves its repo.
403The caller is not on the server's own machine. It is checked first.
404There is no such document, or an attachment's file is not found.
409The document exists only in Kraft's index and was never written to the repo, or it is an attachment of an item that has not started.
501Nothing on the server can open a window, or the editor you named is not installed. The web UI then hands the path to your own machine.

POST /api/work-items/{id}/artifact/open

Opens the pending gate's document in the item's worktree. GET /api/work-items/{id}/artifact carries its absolute_path. The answer is {work_item_id, path, editor}.

FieldTypeDefaultMeaning
editorstring or nullnullAs for POST /api/documents/{id}/open.
StatusWhen
400editor is not a known editor name.
403The caller is not on the server's own machine. It is checked first.
404There is no such item, or the gate has no artifact.
501Nothing on the server can open a window, or the editor you named is not installed.

POST /api/work-items/{id}/artifacts/{kind}/open

Opens a task's document of that kind in the item's worktree. The answer is {work_item_id, path, editor}.

FieldTypeDefaultMeaning
editorstring or nullnullAs for POST /api/documents/{id}/open.
StatusWhen
400editor is not a known editor name.
403The caller is not on the server's own machine. It is checked first.
404There is no such item, or it has no such document there.
501Nothing on the server can open a window, or the editor you named is not installed.

POST /api/work-items/{id}/open-worktree

Opens the item's worktree directory. The answer is {path, editor}.

FieldTypeDefaultMeaning
editorstring or nullnullAs for POST /api/documents/{id}/open.

This route checks the item and its worktree before it checks the caller.

StatusWhen
400editor is not a known editor name.
403The caller is not on the server's own machine. It is checked after the 404.
404There is no such item, or it has no worktree on disk, even for a caller that is not local.
501Nothing on the server can open a window, or the editor you named is not installed.

Storage

GET /api/storage

Answers the last measurement of the run folder, joined to the work items. It measures first when there is none yet, and when ?refresh=1 is given; one walk runs at a time, and a request made during a walk waits for it. A walk can take many seconds on a large instance.

{"measured_at": "...", "state": "held", "used_bytes": 12989000000,
 "quota_bytes": 8589934592, "limit_bytes": 10737418240, "reclaimable_bytes": 9600000000,
 "categories": {"worktrees": 0, "sandboxes": 0, "logs": 0, "results": 0,
                "databases": 0, "attachments": 0, "other": 0},
 "items": [{"id": "...", "title": "...", "status": "completed", "archived": false,
            "bytes": 1200000000, "updated_at": "...", "reclaimable": true}],
 "orphans": [{"name": "...", "bytes": 0}]}
FieldMeaning
stateok, over_quota or held; null with quota_bytes and limit_bytes when storage.worktrees.limit is not set.
used_bytesThe figure the quota and limit govern: the worktrees and the sandbox stores and homes, not logs or databases.
itemsEvery measured folder that has a work item, largest first. reclaimable is true for a completed or abandoned item that is not archived.
orphansEvery measured folder with no work item.
reclaimable_bytesThe sum of the reclaimable items' bytes.

POST /api/storage/preview

Says what archiving the items in ids (1 to 200) would do, and changes nothing. An id given twice counts once.

{"freed_bytes": 2400000000, "used_after_bytes": 10589000000, "state_after": "over_quota",
 "items": [{"id": "...", "title": "...", "bytes": 1200000000, "archivable": true,
            "refusal": null, "uncommitted_files": 3, "unpushed_commits": 0, "branch_kept": false}]}
FieldMeaning
archivablefalse, with refusal, for an id that is unknown, not completed or abandoned, or already archived. It adds nothing to freed_bytes.
uncommitted_filesThe lines of git status --porcelain in the worktree; null when there is no worktree or git cannot read it.
unpushed_commits, branch_keptThe check archive itself makes: commits no other branch, remote-tracking branch or tag holds, and whether the branch stays because of them.
state_afterThe state used_after_bytes would be in; null without a limit.

freed_bytes assumes each worktree goes: an archive whose detached-HEAD rescue fails keeps it and frees nothing. Archiving is POST /api/work-items/bulk with archive.

StatusWhen
422ids is empty or holds more than 200 ids.
Copyright © 2026