Drafts
These are the routes for the drafts behind the Templates and Settings editors, the policy and intake-check routes beside them, one entry per route, and the result every draft route answers. The editors are their callers; a script rarely needs them. Like the other board routes they can change in any release, as Filing, reading and changing work items says. The ops a draft takes are on their own page.
| Route | What it does |
|---|---|
GET /api/drafts | Lists every open draft. |
GET /api/drafts/{area}/{key} | Reads a draft, or the published files when there is none. |
GET /api/drafts/{area}/{key}/fragment?path= | Reads one component of a draft as YAML. |
GET /api/drafts/policy/{key}/preview?chain= | Shows a published chain's caps under the policy draft. |
PUT /api/drafts/{area}/{key}/files/{file} | Stores one file's text as typed. |
POST /api/drafts/{area}/{key}/ops | Applies a list of ops, all or nothing. |
POST /api/drafts/{area}/{key}/ops?preview=1 | Answers what a list of ops would do, and saves nothing. |
POST /api/drafts/{area}/{key}/undo | Undoes the last request. |
POST /api/drafts/{area}/{key}/publish | Writes the draft's files and applies them. |
POST /api/drafts/{area}/{key}/rebase | Keeps the draft's text over files that changed on disk. |
DELETE /api/drafts/{area}/{key} | Discards a draft for good. |
GET /api/policy | Reads policy.yaml from disk. |
PUT /api/policy | Saves policy.yaml without a draft, and applies it. |
GET /api/intake/checks?limit= | Lists what recent intake polls found. |
Areas and keys
The Templates editor keeps unpublished edits on the server, as drafts. There is one draft per area and key. File names are relative to the config directory.
| Area | Key | Files | A publish applies it by |
|---|---|---|---|
chains | A chain id: a lowercase letter, then lowercase letters, digits, _ or - | chains/<key>.yaml | Reloading the library and chains. |
library | library | library.yaml | Reloading the library and chains. |
harnesses | harnesses | harnesses.yaml, policy.yaml | Reloading the policy, only when policy.yaml is in the draft. |
repos | repos | repos.yaml | Nothing: repos.yaml is read when needed. |
policy | policy | policy.yaml | Reloading the policy, as PUT /api/policy does. |
intake | intake | intake.yaml | Rereading intake.yaml and replacing the poller. |
An op can join more files to a chain or library draft. The move_to_library op adds library.yaml to a chain's draft, and renaming a library component adds each chain file it rewrites to the library's. Each joined file records its own base, and a publish writes them all. A PUT also takes any file an op joined, such as the new file of a chain the rename op moved, which keeps its old key.
A settings draft is one of the last four areas. Its problems are every problem its files have, found by the checks the matching PUT route runs, so a draft with none is one that route accepts.
The harnesses and policy drafts may both hold policy.yaml: the first publish wins, and the second answers 409, which rebase resolves. A publish whose apply step fails has already written its files: it answers 500, keeps the draft, and tries again on the next publish.
Every route below that takes {area} and {key} refuses an unknown area with 404, a key the area does not accept with 400, and (for PUT) a file that is not one of the key's files with 422.
Reading drafts
GET /api/drafts
Lists every open draft, newest write first.
[
{
"area": "chains",
"key": "default",
"files": ["chains/default.yaml"],
"changes": 2,
"problems": 0,
"updated_at": "2026-10-07T09:12:44.123456+00:00"
}
]
files are the file names. changes and problems are counts as of the draft's last write.
GET /api/drafts/{area}/{key}
Reads the draft, or, when there is none, the published files and their result.
{
"area": "chains",
"key": "default",
"draft": true,
"files": {"chains/default.yaml": "id: default\n…"},
"base": {"chains/default.yaml": "3b1f…"},
"published": {"chains/default.yaml": "id: default\n…"},
"updated_at": "2026-10-07T09:12:44.123456+00:00",
"result": {"problems": [], "changes": [], "…": "…"}
}
| Field | Meaning |
|---|---|
draft | true when a draft is open. With none it is false, and files are the published ones. |
files | {file: text}: the published files with the draft's text laid over them. A file the draft deletes is null. |
base | {file: digest}: the SHA-256 of each file's text when it joined the draft, null for a file that did not exist. With no draft, the digests of the published files. |
published | The text of each file as it is on disk, null for one that does not exist yet: the left side of a diff. |
updated_at | The draft's last write, or null with no draft. |
result | What the draft resolves to. See The result object. |
addon | On a chains key: {id, version} of the Kraft add-on the chain comes from, else null. |
addon_library | On the library key: the loaded add-ons' components, as {section: {"release:implementer": definition}}. null on every other area. They are never in files. |
An add-on's chain is read under its qualified id: GET /api/drafts/chains/release:ship. It answers 200 as published, with draft false and addon set: it never has a draft. Its file is shown with the add-on's own references written out as release:<name>. Every other route on that key (PUT, ops, undo, publish, rebase, DELETE) answers 409, with <id> comes from add-on <addon id>; extend it or copy it to your library.
Refused with 404 for a chains or library key that has neither a file nor a draft. A settings area answers for a file that is not there yet, because an op creates it.
An unknown {area} answers 404 and a {key} the area does not accept 400, as under Areas and keys.
GET /api/drafts/{area}/{key}/fragment?path=
Reads the YAML of the component at path (a task, a node, a policy block), from the draft when there is one, else from the published file. The text is what the set_fragment op takes back.
{"path": "implementation.main.implement", "text": "id: implement\n…"}
On a repos draft path is a repo's path, and the answer is read-only, since there is no set_fragment for it. The other settings areas have no fragments.
Refused with 400 when path is missing, and with 404 when nothing is at path or the area has no fragments.
An unknown {area} answers 404 and a {key} the area does not accept 400, as under Areas and keys.
GET /api/drafts/policy/{key}/preview?chain=
Shows what a published chain's caps are under the policy draft. The key is policy. It is a read: the draft does not change.
{
"chain": "default",
"scopes": [
{
"path": "default",
"kind": "chain",
"level": "work_item",
"caps": {
"time_cap_minutes": {"value": null, "source": "default", "maximum": null, "exceeds": false}
}
}
]
}
Each scope is the chain itself, then every node, step and task. A gate waits for a person and has none. kind is chain, node, step or task, and level is the policy level it reads its caps at. Every scope lists all four caps (time_cap_minutes, total_time_cap_minutes, token_budget, budget_usd), each with these fields:
| Field | Meaning |
|---|---|
value | What the scope runs under, or null when no layer sets one. |
source | chain, library:<section>.<name>, policy or default. |
maximum | The {value, level} that bounds it at its level, or null for no bound. |
exceeds | Whether value is past maximum. |
Refused with 400 for a key that is not policy, 404 for a chain that is not in the library, 422 for a missing chain, a draft whose policy.yaml does not load or a chain that does not resolve, and 503 when the template library failed to load.
Changing a draft
PUT /api/drafts/{area}/{key}/files/{file}
Stores one file's text as typed, comments kept. The first write makes the draft. A write whose files parse to the published ones drops the draft. {file} is the file's name with its folder, such as chains/default.yaml.
Body: {"text": "…"}. Answers as GET /api/drafts/{area}/{key} does. A text that does not parse is stored, and the answer's result carries a yaml_error.
Consecutive PUTs to one file less than 4 seconds apart are one undo step.
Refused with 422 for a file that is not one of the key's files, unless an op joined it to the draft.
An unknown {area} answers 404, a {key} the area does not accept 400, and a file that is not one of the key's files, unless an op joined it, 422, as under Areas and keys.
POST /api/drafts/{area}/{key}/ops
Applies the ops in the body in order, all or nothing, as one undo step. A chains or library draft is written back whole, so its comments are dropped, and warnings says so until a PUT of that file. The settings areas write over each file's own text, so their comments stay.
Body: {"ops": [{"op": "add_step", …}]}. Answers as GET /api/drafts/{area}/{key} does, plus ops, one {op, result?} per op in the order sent, result being that op's own answer.
{"…": "the fields of the GET answer",
"ops": [{"op": "add_step", "result": {"path": "spec.step_2"}}, {"op": "set_field"}]}
Refused with 409 ("fix the YAML first") when the draft has a yaml_error. An op whose path, container or node addresses an add-on's component answers 409 {detail, op}, naming the add-on.
An op that cannot apply answers 422 {detail, op}, and nothing is saved. op is the op's index in the list, or null when the refusal comes from writing the files back, as when a settings file's layout cannot take the change. A refusal can add keys of its own: a set_fragment that does not parse adds line and col.
An unknown {area} answers 404 and a {key} the area does not accept 400, as under Areas and keys.
POST /api/drafts/{area}/{key}/ops?preview=1
Answers as POST /api/drafts/{area}/{key}/ops does, without saving: the draft, its undo steps and the draft list stay as they were. It takes the same body and is refused in the same cases.
An unknown {area} answers 404 and a {key} the area does not accept 400, as under Areas and keys.
POST /api/drafts/{area}/{key}/undo
Undoes the last request and answers as GET /api/drafts/{area}/{key} does. Undoing back to the published state drops the draft. A draft keeps its last 80 requests. No body.
Refused with 409 when there is nothing to undo.
An unknown {area} answers 404 and a {key} the area does not accept 400, as under Areas and keys.
Ending a draft
POST /api/drafts/{area}/{key}/publish
Writes each file of the draft (a file the draft deletes is removed), applies them as the areas table says, and drops the draft. No body.
{"published": ["chains/default.yaml"], "changes": [], "result": {"…": "…"}}
published is the names of the files written. changes is what was published, as the draft's result.changes listed it. result is the state after, with no draft left.
A publish checks each file against the one on disk when the file joined the draft. If any changed since, for example through a save on the Settings screens, it writes nothing. A file that already holds the draft's text is not counted as changed, so a retry after a 500 answers 200, not 409. The refusal:
{"detail": "published since this draft began: chains/default.yaml",
"files": {"chains/default.yaml": {"published": "…", "draft": "…", "diff": "--- published/chains/default.yaml\n+++ draft/chains/default.yaml\n…"}}}
| Status | When |
|---|---|
404 | There is no draft. |
409 | A file changed on disk since it joined the draft. The body is the example above. |
422 | The draft has a yaml_error or any problem. The body is {detail, problems}. |
500 | The files were written but applying them failed. The draft stays. |
An unknown {area} answers 404 and a {key} the area does not accept 400, as under Areas and keys.
POST /api/drafts/{area}/{key}/rebase
Takes the published files as the draft's new base and keeps its text, so the next publish no longer answers 409. It is the way out of a conflict when the draft should win. No body. It pushes no undo step. Answers as GET /api/drafts/{area}/{key} does.
Refused with 404 when there is no draft.
An unknown {area} answers 404 and a {key} the area does not accept 400, as under Areas and keys.
DELETE /api/drafts/{area}/{key}
Discards the draft for good, with no undo. Answers 204 with no body.
Refused with 404 when there is no draft.
An unknown {area} answers 404 and a {key} the area does not accept 400, as under Areas and keys.
Settings routes
These routes read and save policy.yaml, and read the intake poll log, with no draft. A policy draft is checked the way PUT /api/policy checks a save.
GET /api/policy
Reads policy.yaml from disk. It also answers active_count (the items running now), and its max_concurrent is the value the loaded policy runs under. A body sent back to PUT /api/policy may keep active_count; it is ignored.
PUT /api/policy
Saves policy.yaml and makes the running server use it. The body is merged over the file on disk, top-level key by key: a key the body leaves out is kept, and a key it sends as null is removed. A save never drops what it did not edit. An unparseable file has nothing to keep, so the save replaces it.
| Field | Type | Default | Meaning |
|---|---|---|---|
loops | object | required | The loops: entries, by fix loop. |
default | object | required | The top-level default: block, beside loops: attempts and wall_clock_s. |
max_concurrent | integer | 3, when the file has none either | How many items run at once. At least 1. |
findings, budget, archive | object | the file's | The blocks of the same names. |
triggers | list | the file's | The schedules policy.yaml still holds, which still fire: see When a schedule fires. Schedules otherwise live in intake.yaml. |
rate_limit_retries, auto_escalate_stuck, auto_escalate_stuck_cap, auto_escalate_delay_s | integer or boolean | the file's | The keys of the same names. |
Any other top-level key of policy.yaml is accepted and checked by the loader, such as defaults, maxima and forge_cli_timeout_s. policy.yaml lists every key.
The answer is the saved file's content. The new policy applies at once: a loop cap applies to loops that start after the save, and a counter already running keeps the cap it started with.
| Status | When |
|---|---|
422 | A required field is missing, a field has the wrong type or is out of range (max_concurrent: 0), or the file the merge would write does not load: the answer's detail is the loader's message. Nothing is written. |
GET /api/intake/checks?limit=
Answers what each recent poll found, newest first. limit defaults to 20 and is at most 100; the newest 500 polls are kept. A check is not a work item event.
[{"id": 12, "at": "2026-10-07T09:12:44.123456+00:00", "ready": 1, "started": [],
"skipped": [{"bead": "Kraft-9d8b2", "reason": "epic"}]}]
readyis how many issues in beads were ready.startedis the work item ids the poll filed.skippedis the issues it left alone, each{bead, reason}.reasonis one ofabove_priority_ceiling,already_item,epic,max_concurrent,daily_budget,storageorinvalid_config.storageis while worktrees are over the storage limit.- With
invalid_config,beadisnullwhenpolicy.yamlorrepos.yamldid not load and the poll could not run, and the bead's id when its item could not be filed.
Refused with 422 for a limit outside 1 to 100.
The result object
result is the same on every route that answers a draft:
| Field | Holds |
|---|---|
model | Per file, the mapping it parses to. For a file that does not parse, the newest earlier state that did, else the published file. |
resolved | For a chain, the chain with extends expanded, as GET /api/templates/chains/{id}/resolved shows it, or null when it does not resolve. null for the library. A settings area has its own shape, with its ops. |
problems | [{path, field, message, file, line, col}]: everything a save would refuse, with every schema error listed. See result.problems. |
sources | For a chain that resolves, {path: {field: {value, source}}} for each node, step, task and fix loop: its own fields, and each policy.* cap of its scope. See result.sources. |
changes | [{path, kind, summary, fields?}] against the published files, kind being add, change, remove or rename. See result.changes. |
impact | What a publish reaches. See result.impact. |
warnings | [{file, message}] for a file an edit rewrote whole, whose published text has comments a publish would drop. |
policy_values | {auto_escalate_delay_s, auto_review_attempts}, the instance's values from policy.yaml. |
choices | The closed sets the editors offer, by field: ref (a builtin task's actions), target (a forge task's targets), inputs (what an agent task can ask for) and grants. See result.choices. |
yaml_error | Only when a file does not parse: {file, line, col, message}. |
result.problems
pathis the component's canonical path (implementation.main.implement), andfieldthe key below it.- On the library,
problemslists everythingPUT /api/templates/librarywould refuse, checked against every chain and repo. - Each library problem has
chainorrepo: the one it breaks, with bothnullwhen the problem is inlibrary.yamlitself, wherepathis the component, such astasks.implementer. - Each library problem also has
component: the library component it comes from, ornull. - A repo's problem is at
pathsteeringinrepos.yaml. - A settings draft's problems carry keys of their own, listed with each area's
resolvedanswer on Draft ops.
result.sources
source is:
chain(the file sets it at that path);library:<section>.<name>(the nearest library component it inherits it from, such aslibrary:tasks.implementerorlibrary:nodes.verification);policy(a cap no layer sets, at the value the scope runs under); ordefault.
A subprocess, builtin or forge task leaves out steering and the agent-only caps unless it sets them. With an on_failure recovery it lists those caps with recovery: true, since the recovery runs under them.
result.changes
- A reordered component is a
changewhose summary saysmoved. - A chain-level key that differs (
description,policy) is onechangerow at the chain's own path,"", its summary naming the keys. - A chain the
renameop moved to a new id is onerenamerow at"", withfromandtothe old and new ids. - On the
policyandintakeareas each row is one dotted key of a file,{path, kind, summary, file}.summaryreads "was → now" (not set, orno boundfor a maximum), and a list of mappings (schedules:) is keyed by index (schedules.0.cron). - On those two areas, a file that does not parse, or that the draft deletes, has one file-level row instead,
{path, kind, summary, fields, file},pathbeing the file name. - On the
harnessesarea there is one row per changed file (pathis the file name), thenaccess.<harness>rows (summaryreadsavailable -> never), andharnesses.<id>andprofiles.<name>rows. - On the
reposarea each row is one repo entry:pathis the repo's path, andfieldsthe keys that differ. The file-level row forrepos.yamlstays only when no entry changed, such as a file that does not parse. - On the library,
pathis a component path (tasks.implementer,nodes.verification.review.code_review), and each change addsreaches, the ids of the chains that use the component, as published or as drafted.
result.impact
- For a chain:
{running, repos}, the items still running it and the repos whose default it is. - For the library:
{chains, repos}, the chains any change reaches and the repos whosesteering:names a changed profile. - For
harnesses:{tasks, chains}, the count of tasks whose harness changed state and the ids of the chains they are in. - For
repos:{running: {<repo path>: n}}, the items not ended, per repo path. - For
policyandintake:null.
result.choices
Each of ref, target, inputs and grants is a list of {value, summary}, and a target also has waits, true for one that runs as an external wait.
Related
- Ops for each draft area: the ops
POST /api/drafts/{area}/{key}/opstakes, and a work item's own draft. - Templates and Settings: the screens that call these routes.
- HTTP API: who may call, and which routes are stable.
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.
Ops for each draft area
The ops each kind of draft takes, one entry per op, with the answer each gives, and a work item's own draft.