Skip to content
Takeida
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.

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.
Request
curl "https://your-server.example/v1/agent/c/k7m2xq9d/drafts?limit=50" \
  -H "Authorization: Bearer arb_live_…"
Response200
{
  "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.
Request
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" }'
Response201
{
  "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.

Request
curl https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update \
  -H "Authorization: Bearer arb_live_…"
Response200
{
  "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
summary leaves out the patch text. Full when absent.
documentIduuid
The diff of one document only.
limitinteger · 1 to 200
Documents a page.
cursorstring
nextCursor from 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.
The query is strict. A parameter spelled wrong is refused, rather than ignored and the whole diff sent back as though the filter had worked.
Request
curl "https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update/diff?view=summary" \
  -H "Authorization: Bearer arb_live_…"
Response200
{
  "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.

Request
curl -X DELETE https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update \
  -H "Authorization: Bearer arb_live_…"
Response200
{ "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. 1

    Merges cleanly

    Brought in at once, and counted in updated.

  2. 2

    Collides

    Stored as a conflict, with both sides and where they started.

  3. 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.
Request
curl -X POST \
  https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update/update-from-base \
  -H "Authorization: Bearer arb_live_…"
Response200
{
  "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.

Request
curl https://your-server.example/v1/agent/c/k7m2xq9d/drafts/scribe%2Fquay-update/conflicts \
  -H "Authorization: Bearer arb_live_…"
Response200
{
  "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.
Request
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)."
  }'
Response200
{
  "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.

Request
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.
Request
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 }'
Response200
{
  "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
}

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.

Request
curl "https://your-server.example/v1/agent/c/k7m2xq9d/links/unresolved?draft=scribe/quay-update" \
  -H "Authorization: Bearer arb_live_…"
Response200
{
  "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.

Request
curl https://your-server.example/v1/agent/c/k7m2xq9d/links/orphans \
  -H "Authorization: Bearer arb_live_…"
Response200
{
  "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.

Request
curl https://your-server.example/v1/agent/c/k7m2xq9d/links/deadends \
  -H "Authorization: Bearer arb_live_…"
Response200
{
  "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.
Request
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" }'
Response200
{
  "updated": { "documents": 3, "links": 7 },
  "skipped": [
    {
      "documentId": "5a1e0b77-…",
      "path": "index.md",
      "line": 3,
      "raw": "[[Harbour survey]]",
      "reason": "link_gone"
    }
  ],
  "job": null
}