Browse the reference
Agent · /v1/agent
Drafts, and keeping them current
A draft is where writing happens. It is cut from the trunk, or from another draft, and starts identical to it. Everything written on it stays there until a proposal carries it to its base.
Bases move on while a draft is open. Catching up brings in what changed, applies whatever merges cleanly, and leaves only what truly collides to be answered, one conflict at a time.
- mainthe trunkApproved
- ana/tide-tablesin reviewProposed·#41
- scribe/quay-update2 behind its baseUnapproved
- scribe/berth-photoscut from a draftUnapproved
Drafts
A draft is named as a namespace and a name, in lowercase: scribe/quay-update. Routes take that name in place of {id}, with its slash encoded as %2F, or the draft's id. In a query, ?draft=scribe/quay-update is written as it is.
List drafts
GET/v1/agent/c/{identifier}/drafts
Every draft in the Concept, the trunk first and then newest first. Seeing drafts needs a writer's standing or above: a reader sees only what has been approved.
What a draft tells you
- changedDocumentsinteger
- How many documents this draft has changed. The size of its own work, not of the Concept.
- newDocuments, pendingDeletionsinteger, integer
- What the draft adds, and what it will remove once published.
- stale, behindByboolean, integer
- Whether its base has moved on, and by how many published entries.
- approvalStatus, proposalNumberstring, integer · null
- How far it has got, and the proposal carrying it, if one is open.
- autoUpdateboolean
- Whether it catches up by itself each time something is published.
curl "https://your-server.example/v1/agent/c/k7m2xq9d/drafts?limit=50" \
-H "Authorization: Bearer arb_live_…"{
"items": [
{ "name": "main", "isTrunk": true, "approvalStatus": "approved", … },
{ "name": "scribe/quay-update", "isTrunk": false, "stale": true, "behindBy": 2, … }
],
"nextCursor": null
}Create a draft
POST/v1/agent/c/{identifier}/drafts
Cuts a draft. It belongs to the agent, and holds nothing of its own until something is written on it.
Body
- namestring · 3 to 120 · required
- A namespace, a slash and a name. Letters, digits, dots, hyphens and underscores, in lowercase.
- basestring
- The draft to cut from. The trunk when absent.
- pinnedboolean
- Keeps it at the top of its owner's list.
Refusals
- 409draft_existsA draft already has that name.
- 409draft_too_deepThe base is as many drafts from the trunk as the Concept allows.
- 402plan_limitThe agent's owner has as many open drafts here as the plan allows. An agent's drafts count as its owner's.
curl -X POST https://your-server.example/v1/agent/c/k7m2xq9d/drafts \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{ "name": "scribe/quay-update" }'{
"id": "b81e77a4-51c9-4f0e-8a3d-6c2f0a9d4e77",
"name": "scribe/quay-update",
"baseBranch": "main",
"isTrunk": false,
"depth": 1,
"changedDocuments": 0,
"newDocuments": 0,
"pendingDeletions": 0,
"stale": false,
"behindBy": 0,
"approvalStatus": "unapproved",
"proposalNumber": null,
"pinned": false,
"autoUpdate": true,
"owner": { "type": "API_KEY", "id": "…", "name": "Scribe", "agentSlug": "scribe" },
"createdAt": "2026-10-09T11:02:41.000Z",
"publishedAt": null
}Read a draft
GET/v1/agent/c/{identifier}/drafts/{id}
One draft, described as the list describes it.
curl https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update \
-H "Authorization: Bearer arb_live_…"{
"id": "b81e77a4-51c9-4f0e-8a3d-6c2f0a9d4e77",
"name": "scribe/quay-update",
"baseBranch": "main",
"isTrunk": false,
"depth": 1,
"changedDocuments": 3,
"newDocuments": 1,
"pendingDeletions": 0,
"stale": true,
"behindBy": 2,
"approvalStatus": "unapproved",
"proposalNumber": null,
"pinned": false,
"autoUpdate": true,
"owner": { "type": "API_KEY", "id": "…", "name": "Scribe", "agentSlug": "scribe" },
"createdAt": "2026-10-09T11:02:41.000Z",
"publishedAt": null
}Compare it with its base
GET/v1/agent/c/{identifier}/drafts/{id}/diff
What a draft changes against its base, document by document. A diff is the one answer here with no natural size, so ask for the summary first and fetch patches only for what needs reading.
Query
- viewfull · summary
summaryleaves out the patch text. Full when absent.- documentIduuid
- The diff of one document only.
- limitinteger · 1 to 200
- Documents a page.
- cursorstring
nextCursorfrom the page before.
What a document tells you
- kindadded · modified · deleted · renamed
- What happened to it.
- bodyDiffstring
- A unified diff of the Markdown. Properties are not in it.
- frontmatterDiffarray
- Each property that changed, before and after.
curl "https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update/diff?view=summary" \
-H "Authorization: Bearer arb_live_…"{
"base": "main",
"head": "scribe/quay-update",
"documents": [
{
"documentId": "0d9a4c5e-…",
"kind": "modified",
"path": "field-notes/2026/harbour-survey.md",
"previousPath": null,
"title": "Harbour survey",
"bodyDiff": "",
"frontmatterDiff": [],
"additions": 9,
"deletions": 6
},
{
"documentId": "e7d6c5b4-…",
"kind": "added",
"path": "field-notes/2026/quay-depths.md",
"previousPath": null,
"title": "Quay depths",
"bodyDiff": "",
"frontmatterDiff": [ { "key": "tags", "before": null, "after": ["survey"] } ],
"additions": 31,
"deletions": 0
}
],
"totals": { "documents": 2, "additions": 40, "deletions": 6 },
"nextCursor": null
}With view=summary, bodyDiff is empty. Leave it out to receive the patches.
Delete a draft
DELETE/v1/agent/c/{identifier}/drafts/{id}
Destroys a draft and the work on it. Files that existed only on the draft go with it. It is the one draft action that loses something, so a Portage key cannot do it, and an agent may do it only where its role lets it delete that draft.
curl -X DELETE https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update \
-H "Authorization: Bearer arb_live_…"{ "ok": true }Catching up with the base
Catching up never stops at the first collision. Every document that merges cleanly is brought in. What collides is stored, can be read back later, and is answered one at a time. Only when nothing is left does the draft count as current.
1
Merges cleanly
Brought in at once, and counted in updated.
2
Collides
Stored as a conflict, with both sides and where they started.
3
Last one answered
The draft counts as current, and caughtUp is true.
Catch it up with its base
POST/v1/agent/c/{identifier}/drafts/{id}/update-from-base
Brings in what has been published to the base since this draft was cut or last caught up. Some documents updated and some in conflict is a normal result, not an error.
A conflict
- kindbody · frontmatter_key · path · delete_edit · add_add
- What collided: the text, one property, where the document lives, an edit against a deletion, or two documents made at one path.
- base, theirs, minestring · null
- The text both sides started from, what the base has now, and what the draft has.
curl -X POST \
https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update/update-from-base \
-H "Authorization: Bearer arb_live_…"{
"updated": 4,
"documents": [
{ "documentId": "7c2f9e10-…", "status": "updated" },
{ "documentId": "0d9a4c5e-…", "status": "conflicted" }
],
"conflicts": [
{
"id": "c0ffee00-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"documentId": "0d9a4c5e-…",
"path": "field-notes/2026/harbour-survey.md",
"kind": "body",
"frontmatterKey": null,
"base": "Berth 4 reads 5.9 m.",
"theirs": "Berth 4 reads 6.0 m.",
"mine": "Berth 4 reads 6.2 m at low water."
}
]
}With nothing to bring in, updated is 0 and both lists are empty.
List what is left to answer
GET/v1/agent/c/{identifier}/drafts/{id}/conflicts
The conflicts not yet answered, oldest first. Answered ones are kept as a record and are not listed.
curl https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update/conflicts \
-H "Authorization: Bearer arb_live_…"{
"items": [
{
"id": "c0ffee00-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"documentId": "0d9a4c5e-…",
"path": "field-notes/2026/harbour-survey.md",
"kind": "body",
"frontmatterKey": null,
"base": "Berth 4 reads 5.9 m.",
"theirs": "Berth 4 reads 6.0 m.",
"mine": "Berth 4 reads 6.2 m at low water.",
"resolved": false
}
]
}Answer one conflict
POST/v1/agent/c/{identifier}/drafts/{id}/conflicts/{conflictId}/resolve
Decides what the document says. Keep the draft's side, take the base's, or write what it should be.
Body
- resolutionmine · theirs · manual · required
- The draft's text, the base's text, or your own.
- contentstring · up to 4 MB
- The text to keep, with
manual.
curl -X POST \
https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update/conflicts/c0ffee00-…/resolve \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{
"resolution": "manual",
"content": "Berth 4 reads 6.2 m at low water (6.0 m on the October chart)."
}'{
"resolved": true,
"remaining": 0,
"caughtUp": true
}caughtUp is true when that was the last one: the draft now counts as current.
Abandon the catch-up
DELETE/v1/agent/c/{identifier}/drafts/{id}/conflicts
Clears the unanswered conflicts and decides nothing. The draft still reports itself behind, and the next catch-up meets the same collisions again.
curl -X DELETE \
https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update/conflicts \
-H "Authorization: Bearer arb_live_…"204 · no body
Choose whether it keeps itself current
PATCH/v1/agent/c/{identifier}/drafts/{id}/settings
A new draft catches up by itself each time something is published to the trunk. Turn that off and it falls behind on purpose. It also turns itself off the first time an automatic catch-up meets a real conflict, so the same collision is not met on every publish.
Body
- autoUpdateboolean · required
- On or off. Turning it on does not replay what was missed: catch up once to do that.
curl -X PATCH \
https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update/settings \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{ "autoUpdate": false }'{
"id": "b81e77a4-51c9-4f0e-8a3d-6c2f0a9d4e77",
"name": "scribe/quay-update",
"baseBranch": "main",
"isTrunk": false,
"depth": 1,
"changedDocuments": 3,
"newDocuments": 1,
"pendingDeletions": 0,
"stale": true,
"behindBy": 2,
"approvalStatus": "unapproved",
"proposalNumber": null,
"pinned": false,
"autoUpdate": false,
"owner": { "type": "API_KEY", "id": "…", "name": "Scribe", "agentSlug": "scribe" },
"createdAt": "2026-10-09T11:02:41.000Z",
"publishedAt": null
}Links
What a Concept's links say about its shape. Each of the three lists takes ?draft=, reads the trunk without it, and is paged.
List links that point nowhere
GET/v1/agent/c/{identifier}/links/unresolved
Every link whose target does not exist, with the document and line it is on. Each is a document somebody meant to write, or a name that has since changed.
curl "https://your-server.example/v1/agent/c/k7m2xq9d/links/unresolved?draft=scribe/quay-update" \
-H "Authorization: Bearer arb_live_…"{
"items": [
{
"fromDocumentId": "5a1e0b77-…",
"fromPath": "index.md",
"raw": "[[Dredging plan]]",
"targetName": "Dredging plan",
"line": 11
}
],
"nextCursor": null
}List documents nothing links to
GET/v1/agent/c/{identifier}/links/orphans
Documents no other document links to. Somebody reading will not come across them.
curl https://your-server.example/v1/agent/c/k7m2xq9d/links/orphans \
-H "Authorization: Bearer arb_live_…"{
"items": [
{
"documentId": "9a8b7c6d-…",
"path": "scratch/old-tide-notes.md",
"title": "Old tide notes"
}
],
"nextCursor": null
}List documents that link to nothing
GET/v1/agent/c/{identifier}/links/deadends
Documents that link to no other. Somebody who arrives has nowhere to go next.
curl https://your-server.example/v1/agent/c/k7m2xq9d/links/deadends \
-H "Authorization: Bearer arb_live_…"{
"items": [
{
"documentId": "e7d6c5b4-…",
"path": "field-notes/2026/quay-depths.md",
"title": "Quay depths"
}
],
"nextCursor": null
}Mend the links a move broke
POST/v1/agent/c/{identifier}/links/rewrite
Accepts the offer a move made. Every link that pointed at the old path is rewritten to the new one, each as a new version of the document it is in.
It is a separate step because a move may mean to break links: renaming a document away and making a new one under the old name is a fair thing to do.
Body
- updateTokenuuid · required
- The token the move returned. It works on the draft the move was made on, and only until it expires.
What comes back
- skipped[].reasonlink_gone · document_gone · excluded
- Why a link was left alone: it changed after the move, its document is gone, or the agent cannot read that document.
Refusals
- 404not_foundNo such token, or it was issued on another draft.
- 410token_expiredThe offer has lapsed.
- 409token_usedIt has been redeemed already, so the work is done.
curl -X POST \
"https://your-server.example/v1/agent/c/k7m2xq9d/links/rewrite?draft=scribe/quay-update" \
-H "Authorization: Bearer arb_live_…" \
-H "Content-Type: application/json" \
-d '{ "updateToken": "f1e2d3c4-b5a6-4978-8a69-7b5c4d3e2f10" }'{
"updated": { "documents": 3, "links": 7 },
"skipped": [
{
"documentId": "5a1e0b77-…",
"path": "index.md",
"line": 3,
"raw": "[[Harbour survey]]",
"reason": "link_gone"
}
],
"job": null
}