Proposed changes
A proposal is a change to an element that waits for a keeper’s word. Any member whose key reads the element can propose one: the fields they would set, a line of evidence, and how long it stands. The element does not change. The world’s owner or a co-builder then accepts it, which writes the element as their own write, or declines it; the proposer can withdraw it. Nothing about a proposal reaches the change feed: only the accept’s write does, as one row by the keeper.
A world’s owner can also turn on AI seats propose, a person accepts on the world page. Under that switch every change an agent seat makes to an existing element becomes a proposal instead of a write, and the seat’s new elements carry a mark saying they are proposed. A person’s key is never held by the switch.
Propose
Section titled “Propose”POST /api/v2/{type}/{id}/proposals with {"fields": {...}, "evidence": "...", "expires": 30}. Any member key that reads the element may call it, a read key included; the owner uses the world’s write key and PIN. The answer is 201 {"proposal": {...}}; the element is unchanged.
A proposal carries kind ("proposal"), id, element_type, element_id, state (open, lapsed, accepted, declined), fields (what it would set: since 2026-10-23, keel #565, only the fields whose proposed value differs from the element’s), omitted (the fields a PUT left out that were not empty on the element: proposed empty, so a card can say “left empty by this replace”; [] for a PATCH, a link edit or this route), base (the element’s values for the fields the write named when it was made) and base_seq (the element’s change_seq then), evidence, expires, by (a membership id, or "owner"), via and client (the door it came through), decided_by, decided_at, decline_reason, created_at, updated_at.
evidenceis 1 to 2,000 characters.expiresis 1 to 365 days, default 30; a row past its date reads aslapsedon every read and can still be accepted or declined.fieldsfollows the element’s own field rules. A field the caller cannot see (one a known-of stub withholds) is422 field_not_visible. A link to an id the world does not hold is400 invalid_link, as on any write.- Five open proposals per element per proposer and fifty per world: past either,
422 too_many_open. Lapsed rows do not count. Idempotency-Keyis honoured as on a create.- An element the caller cannot see, and the keyless public view, answer
404; a read key that belongs to no member answers403.
GET /api/v2/{type}/{id}/proposals lists an element’s proposals: keepers see every row, other members their own.
A proposal with nothing that differs is refused: 422 invalid_request, param fields. Under the switch, a seat’s write that changes nothing makes no proposal and answers 200 with the element as it is.
Accept, decline, withdraw
Section titled “Accept, decline, withdraw”- Accept:
POST /api/v2/{type}/{id}/proposals/{pid}/accept, by the owner or a co-builder with the PIN. The answer is200 {"proposal": {...}, "element": {...}}. The write is the keeper’s own:changed_bynames the keeper, the element takes one newchange_seq, and the feed gets one row. A keeper cannot accept their own proposal (403 own_proposal). If a proposed field moved since the proposal was made, nothing is written:409 stale_proposalwithconflicts: [{"field", "base", "current"}]and the element undercurrentbeside the envelope; only a moved proposed field counts, so a change elsewhere on the element does not block an accept. A link that died in the meantime is400 invalid_linkand the row stays open. - Decline:
POST .../proposals/{pid}/declinewith an optional{"reason": "..."}, by the owner or a co-builder:200with the row. - Withdraw:
DELETE .../proposals/{pid}by the proposer:204. A keeper on another member’s row is403; anyone else404. - Deciding a row already decided is
422 invalid_request. Deleting an element drops its undecided rows and keeps the decided ones.
The queue
Section titled “The queue”GET /api/v2/world/proposals?state=open&since=&element=&limit=&cursor= answers {"data": [...], "has_more": ..., "next_cursor": ...}, newest first. state is open (the default), lapsed, accepted, declined or all; since filters on updated_at; element on one element’s id. Keepers see every row, other members their own.
Since 2026-10-23 (keel #565) the queue holds two kinds of row, told apart by kind: proposals ("proposal"), and every element a seat made under the switch that still carries its x_review_proposed mark ("new_element": id and element_id the element’s, element_type, state open or lapsed from the mark’s expires, by, evidence, expires, and the element’s created_at and updated_at). A marked element is listed under open, lapsed and all, never accepted or declined; at most the 500 newest are read per request. Both kinds come in one order, newest first; the cursor is opaque, so pass next_cursor back as it came.
The queue carries an ETag of "<newest updated_at>:<open count>:<marked count>" (an element’s own list keeps "<newest updated_at>:<open count>" and holds proposals only); send it back as If-None-Match and an unchanged queue answers 304 with no body, refunded against your request budget as the head poll is. Poll the queue, not the feed, to learn of new proposals: no proposal act writes a feed row.
The switch
Section titled “The switch”agent_writes_proposed on GET /world is true when the owner has turned on AI seats propose, a person accepts on the world page. It is read-only on the API and off by default; a PATCH /world carrying it is refused like public_read.
With it on, for an agent seat’s key only:
PATCH,PUTand a link edit on an existing element answer202 {"proposal": {...}}instead of writing.If-Matchis checked first: a stale one is still412. The proposal’s evidence is the seat’s client name, or else its door.- A
/bulkitem answers a slot{"status": 202, "id": ..., "proposal": {...}}. - The MCP’s
update_element,edit_linksandbulk_applyanswer{"proposed": true, "proposal": {...}}. - A new element is created and carries
x_review_proposed({by, evidence, expires}, the review profile’s mark), and waits in the queue as anew_elementrow. A keeper settles it withPOST /api/v2/{type}/{id}/review{"decision": "accept" | "decline", "reason": "..."}(the owner or a co-builder, with the PIN):acceptclears the mark, one keeper write,200{"element": ...};declinedeletes the element,200{"deleted": id}, the reason answered back and kept nowhere. An element with no mark is422not_proposed; the member the mark names gets403proposed_only. No MCP tool settles a mark. - A seat deletes only what it created; deleting anything else, and a seat’s accept or decline, answer
403 proposed_only.
Not held by the switch: adopt, hide and unhide, and files. A PUT proposal is a whole replace: schema fields the body leaves out that are not empty are proposed empty and named in omitted, and extension keys are merged at accept. An accept writes exactly fields, and a stale check reads only those, so a keeper’s edit to another field survives. A guest’s proposal keeps the links it cannot see.
In the clients
Section titled “In the clients”The TypeScript SDK and the Python package carry the six calls and type the 202: a write that was turned into a proposal is raised as its own error (OwProposedError with isProposed; ProposedError with is_proposed) carrying the proposal, never returned as if the element had changed; a /bulk slot keeps its 202 as the document shapes it. See TypeScript and Python.
