Preview. These docs describe the interface WIRK launches with. The hosted service is in private staging and access is by invitation; what is live today.
write
write changes items and links in one atomic batch: every operation applies, or none does.
Request
POST /v2/write
Authorization: Bearer <token>
{"request_id": "w-3f9a2c41d0",
"expect": {"5c1e7a90": 3},
"operations": [Operation, …]}
| Key | Notes |
|---|---|
operations | Required. 1–32, applied in order, all or nothing |
request_id | 1–200 characters. Required on the wire; the CLI and MCP server make one when you leave it out, and print it |
expect | The revision you read of each record the batch depends on; at most 64 entries |
mode | "apply" (default) or "propose" |
reason | Required to propose, to archive, to override a duplicate, and to complete work (the evidence). At most 2,048 characters |
workspace_id | Optional; default: your only wirkspace |
format | "text" (default) or "json" |
Operations
| Operation | Shape |
|---|---|
item.create | {"op": "item.create", "ref": "new", "data": ItemData, "allow_duplicate_of": ["ID"]} (ref and allow_duplicate_of optional) |
item.edit | {"op": "item.edit", "id": "ID", "patch": ItemPatch} |
item.archive | {"op": "item.archive", "id": "ID"}, with the write's reason |
item.restore | {"op": "item.restore", "id": "ID"} |
link.create | {"op": "link.create", "data": {"type": "…", "from": "ID", "to": "ID", …}} |
link.remove | {"op": "link.remove", "id": "LINK_ID"} |
ItemData (create): title, body, work or context (at most one), fields, uploads (upload IDs), folder_id, is_folder.
ItemPatch (edit): title, body (replaces the body), work, context, fields, attach_uploads, detach_file_ids, folder_id.
workis{"owner_id": "…", "due_at": "2026-10-15T17:00:00Z", "criteria": [{"text": "…"}]}, every key optional. Givingworkmakes an item of kindwork;{}is enough. An edit'sworkmerges: changing the owner keeps the criteria and due date."work": nullremoves it.owner_idmust be a member ("me"means you);nullunassigns.contextis{"level": "organization" | "initiative", "state": "active", "steward_id": "alice", "open": false}; onlylevelis required. It makes an item of kindcontext. An initiative's state defaults toactive;steward_idnames who maintains it (default: you, or the person whose agent you are);open: truelets everyone with edit rights change it. On edit, sendlevelunchanged; omittedsteward_idandopenkeep their values. Creating context is for administrators and their agents; anyone else's write is refused withrequires_reviewand should be proposed.- An item with neither
worknorcontextis adoc, or a folder with"is_folder": true. fieldsis the same flat map as in query:{"status": "completed", "priority": "low"}, a list for a many-value field,nullto clear.refnames a new item so later operations in the same batch can point at it as"$new".
Links
link.create data by type:
| Type | Data |
|---|---|
related_to | {"type": "related_to", "from": A, "to": B} |
contributes_to | {"type": "contributes_to", "from": CHILD, "to": PARENT, "contribution": "…", "criteria": [{"text": "…"}]}. contribution and criteria are optional; an initiative takes contribution only |
requires | {"type": "requires", "from": WORK, "to": PREREQUISITE, "criterion_id": "…"} (criterion_id optional; work to work only) |
cites | {"type": "cites", "from": A, "to": EVIDENCE, "target_revision": 2, "selector": Selector, "relation": "supports" | "contradicts" | "background", "quotation": "…"} (quotation optional) |
relies_on | {"type": "relies_on", "from": A, "to": SOURCE, "target_revision": 3, "assumption": "one line"} |
A selector says which part of the cited item is meant:
| Selector | Shape |
|---|---|
| The whole body | {"type": "whole", "part": {"type": "body"}} |
| A whole file | {"type": "whole", "part": {"type": "file", "file_id": "…"}} |
| A stretch of text | {"type": "text", "part": {"type": "body"}, "start": 120, "end": 310} |
| Lines | {"type": "lines", "part": {"type": "body"}, "first": 4, "last": 9} |
| Pages of a file | {"type": "pages", "file_id": "…", "first": 2, "last": 3} |
| A stretch of audio or video | {"type": "time", "file_id": "…", "start_ms": 61000, "end_ms": 95000} |
| A region of an image | {"type": "region", "file_id": "…", "x": 0.1, "y": 0.2, "width": 0.5, "height": 0.3} |
expect
expect maps each record to the revision you read, the rN on its card or fetch: {"5c1e7a90": 3}. Full or short IDs work. Include:
- every item you edit, archive or restore;
- the
fromitem of every link you create; - the parent work of a
contributes_tolink (not an initiative: linking to one needs only read access).
Items created in the same batch ($new) need no entry, and neither does a link you remove. Every entry is checked, even for a record no operation touches. If any has moved on, nothing is applied and the answer names the current revision. In the CLI, ID@N fills expect for you.
Completing work
Set the status to a completed value and give the evidence: text naming the tests that pass, a commit, a link or a file path, or a file attached or cited in the same write. Anyone in the wirkspace may complete work. In the CLI the evidence is --evidence; in MCP and HTTP it is the write's reason. The service stores it with who completed the work and shows it at full depth (Reason (r4): …); it never judges it.
Apply or propose
- Apply (the default) when a person asked for the change, including completing work.
- Propose (
"mode": "propose"with areason) when you act without a person's direction, as a background or scheduled agent. The write is stored as a proposal for a person to review; nothing changes until it is accepted. Nobody accepts their own proposal.
Examples
IDs are illustrative.
A progress doc linked to two items
wirk write new 'Webhook retries: progress, 1 October' --body-file progress.md --link related_to:5c1e7a90 --link related_to:8d24f6b1
{"request_id": "w-3f9a2c41d0",
"operations": [
{"op": "item.create", "ref": "new", "data": {"title": "Webhook retries: progress, 1 October", "body": "…"}},
{"op": "link.create", "data": {"type": "related_to", "from": "$new", "to": "5c1e7a90"}},
{"op": "link.create", "data": {"type": "related_to", "from": "$new", "to": "8d24f6b1"}}]}
Applied w-3f9a2c41d0 · 3 operations
created 9e4c21f7 ($new) · doc — Webhook retries: progress, 1 October
linked 9e4c21f7 related_to ↔ 5c1e7a90 · link 4d6a0e19
linked 9e4c21f7 related_to ↔ 8d24f6b1 · link a5c97b32
revisions: 9e4c21f7 r1
next: query 9e4c21f7
A linked doc is the cheap way to record progress; prefer it to rewriting an item's body.
Completing work, with evidence
wirk write edit 5c1e7a90@3 status=completed --evidence 'Fixed in 4f2a9c1; tests/test_webhooks.py::test_retry_backoff passes'
{"request_id": "w-8b1d07e2c4", "expect": {"5c1e7a90": 3},
"reason": "Fixed in 4f2a9c1; tests/test_webhooks.py::test_retry_backoff passes",
"operations": [{"op": "item.edit", "id": "5c1e7a90", "patch": {"fields": {"status": "completed"}}}]}
Applied w-8b1d07e2c4 · 1 operation
edited 5c1e7a90 r3 → r4 · status completed · evidence recorded
next: query 5c1e7a90
With a file as the evidence: upload it, then wirk write edit 5c1e7a90@3 status=completed --upload upload_7e8f9a0b1c2d4e5f8a9b0c1d2e3f4a5b --evidence 'Load test log attached'.
A task under an initiative, taken by you
wirk write new 'Rate-limit the public API' owner=me --criterion 'Limits are per partner' --criterion 'A 429 names the retry time' --link contributes_to:2f9b3c4e
Applied w-1c5e9a7b30 · 2 operations
created 8d24f6b1 ($new) · work — Rate-limit the public API
linked 8d24f6b1 contributes_to → 2f9b3c4e · link 9a0b1c2d
revisions: 8d24f6b1 r1
next: query 8d24f6b1
owner= and --criterion make the new item work. Under parent work rather than an initiative, add the parent's revision: --link contributes_to:2f9b3c4e@7.
Archiving, with a reason
{"reason": "Superseded by 9e4c21f7",
"expect": {"3b8e1d42": 2},
"operations": [{"op": "item.archive", "id": "3b8e1d42"}]}
The same completion, proposed by a background job
A job that watches pull requests sees the fix merge. Nobody asked it to act, so it proposes; the reason's first line becomes the proposal's title.
{"request_id": "nightly-20261002-7", "mode": "propose",
"reason": "Mark webhook retries completed\n\nThe retry fix merged in 4f2a9c1 and the retry tests pass.",
"expect": {"5c1e7a90": 3},
"operations": [{"op": "item.edit", "id": "5c1e7a90", "patch": {"fields": {"status": "completed"}}}]}
Proposed nightly-20261002-7 · 1 operation for review
proposal c4a1e902 r1 — Mark webhook retries completed
next: query c4a1e902
Receipts
Text receipts list one line per result, then the final revision of every changed item and the one fetch that shows the effect. JSON carries full IDs, and revisions is exactly the expect map for your next write:
{"ok": true,
"data": {"request_id": "w-8b1d07e2c4", "write_state": "applied",
"results": [{"resource": "item", "id": "item_5c1e7a90…", "revision": 4, "operation_index": 0}],
"defaults": [], "revisions": {"item_5c1e7a90…": 4}},
"errors": [], "notices": [], "page": {"complete": true, "next_cursor": null}, "timing_ms": {"total": 1.4}}
Receipts may suggest links for you to confirm, each as a runnable line, for example confirm one: write link 8d24f6b1@1 contributes_to 2f9b3c4e@7. Only links you write are stored. Writing a context item adds the notice context_parsed (the parts found) and, over the authoring target, context_long.
Retries
After an uncertain result (a timeout, a dropped connection), resend the identical body with the same request_id. It applies once, or returns the stored receipt with the notice replayed and the line stored receipt: this retry changed nothing. The CLI resends once on its own and prints the exact retry otherwise. You can also look the receipt up: wirk query receipt=w-8b1d07e2c4.
Refusals
Nothing is applied when a write is refused. The text names the operation and the fix:
Not applied w-8b1d07e2c4 · basis_changed: 5c1e7a90 is at r5; you read r3
Fetch it again (query 5c1e7a90), check that your change still applies, and resend under a new request_id.
Not applied w-8b1d07e2c4 · reason_required: Completing work needs its evidence: a note naming the tests that pass, a file path or a link, or a file attached or cited in this write
Not applied w-6e2d4b8a10 · likely_duplicate at operations[0] (item.create): 71f0c8ae looks like the same work
Use 71f0c8ae, or rerun with --allow-duplicate-of 71f0c8ae --reason REASON (MCP: "allow_duplicate_of": ["71f0c8ae"] on the item.create, and a "reason")
Others: not_available (no readable record has that ID), ambiguous_ref, unknown_owner (with the members as choices), unknown_enum_field and unknown_enum_option (with the allowed values), invalid_context, invalid_link, requires_review (context someone else maintains: propose instead), request_conflict. All codes: Errors.
Files
Upload first, then attach:
wirk upload report.pdf --description 'Load test results'
Uploaded report.pdf · application/pdf · 2.1 MB · sha256 4a5b6c7d
attach with: write new report.pdf --upload upload_7e8f9a0b1c2d4e5f8a9b0c1d2e3f4a5b
To attach to an existing item: wirk write edit ID@N --upload upload_…, or "patch": {"attach_uploads": ["upload_…"]}. Anyone who can edit the item can attach any upload in the wirkspace. How uploads work over HTTP: HTTP API.