Skip to content

Canvas Roles

Scene format

A canvas's scene is Excalidraw-compatible. Keys NEXUS does not model round-trip unchanged — a scene exported from NEXUS opens in Excalidraw, and a scene saved from Excalidraw and brought back into NEXUS keeps every field NEXUS itself doesn't use.

Role ladder

Roles are ordered. Each role includes everything the roles below it can do:

RoleCan readCan propose a writeCan apply a write directly
vieweryesnono
commenteryesyesno
editoryesyesyes
owneryesyesyes

There is no default role. An agent that has not been explicitly granted a role on a canvas gets a clear error when it calls a canvas tool against that canvas — not a silent no-op, and not a downgraded read.

The write-outcome contract

Every canvas write tool (see MCP Tools) returns exactly one of four outcomes. The four are always distinguishable from one another — a caller never has to infer which one happened from prose in a message string.

OutcomeCarriesWhen
appliedthe new revisionThe caller has editor or owner, and the write went through.
proposeda proposal idThe caller has commenter, and the write is pending approval.
conflictboth the expected and current revisionThe caller's expectedRevision is stale.
rejecteda reasonThe caller's role is viewer, or the reason argument was malformed.

A conflict is never reported as success. Code that only checks for a thrown error will mishandle a conflict — check the outcome explicitly.

Optimistic concurrency

Every write carries an expectedRevision. If it does not match the canvas's current revision, the call returns conflict rather than applying against a scene the caller has not actually seen:

json
{
  "jsonrpc": "2.0",
  "id": 20,
  "result": {
    "resultType": "tools/call",
    "content": [{
      "type": "text",
      "text": "{\"outcome\":\"conflict\",\"expectedRevision\":41,\"currentRevision\":43}"
    }],
    "isError": false
  }
}

The correct response to a conflict is to re-read the canvas's current state, recompute the intended change, and resubmit with the new revision — never to resubmit the same write with the revision number simply bumped. See How to handle canvas conflicts.

The reason field

Every write carries a reason: a single plain line describing the change. A reason containing a newline or a control character is rejected outright, not sanitized — this is a deliberate injection defense, since the field is shown to humans in the audit trail and can be fed back into another agent's context. Silently stripping the offending characters would let a disguised payload through in weakened form; rejecting it forces the caller to send a clean line instead.

json
{
  "jsonrpc": "2.0",
  "id": 21,
  "result": {
    "resultType": "tools/call",
    "content": [{
      "type": "text",
      "text": "{\"outcome\":\"rejected\",\"reason\":\"reason must be a single line with no control characters\"}"
    }],
    "isError": false
  }
}

Workspace scoping

Canvas reads and writes are scoped to a single workspace. A write naming a different workspace than the one the calling agent is operating in is refused.

Proposals and approval

A commenter's write becomes a proposal rather than an immediate change. A proposal can be approved — via canvas_approve_proposal — by a human, or by any agent holding editor or above. It can equally be rejected via canvas_reject_proposal. The audit trail records the proposer and the approver as two separate identities, even when approval comes from an agent, so "who asked for this" and "who signed off" are always reconstructable from the record itself.

Roadmap

The canvas is not yet a splittable window pane, and comments cannot yet be persisted independent of a proposal. No standalone comment tool is exposed as a result — proposing a change is currently the only structured feedback mechanism available to a commenter.

Built with purpose.