12. Graph annotation writes — bounded local writes after federated selection
A realm may declare that a type’s REAL nodes accept bounded local annotations: mark each writable
property annotatable: true, optionally mark one property annotationVersion: true as the
concurrency field, and the type must have an identity: true property. Deployments additionally
gate the whole surface off by default. Without both opt-ins, no write is possible.
# types/person.yml (excerpt)properties: id: { type: string, identity: true } reviewStatus: { type: string, annotatable: true } reviewedAt: { type: string, annotationVersion: true }An annotation statement is: any read selection (it may traverse fetched, source-backed nodes), then
exactly WITH DISTINCT <target>[, <expr> AS <alias>…] ORDER BY … LIMIT n, then exactly
SET <target>.<property> = <literal | $parameter | alias>:
MATCH (u:AssistantUser)-[:EMAILED]->(p:Person)-[:HAS_X]->(v:SomeFetchedLabel)WHERE v.field = 'value'WITH DISTINCT p ORDER BY p.id LIMIT 50SET p.reviewStatus = 'follow-up'RETURN pGuarantees.
- The fetched node participates only in selection. The transient materialization rolls back as always; the selected real nodes are then updated in a separate write.
- The limit must be a positive whole number within the deployment maximum; the ordering is
required. Everything else — writing a fetched binding,
SET +=, dynamic property names, label changes,CREATE/MERGE/DELETE/REMOVE/FOREACH, procedure calls, subqueries,UNION— is rejected before anything runs. Writing a fetched binding names the durable binding you probably meant, and guarantees no fetch and no write occurred. - A write never redirects to the source system, even if the source declares a writable operation. Changing the source record is always its own explicit action.
- Execution is two-phase: a dry-run returns the exact would-be changes under an expiring plan id; confirming applies that frozen plan — never a re-run selection. An expired plan requires a new dry-run; a plan applies at most once.
- At apply time each target is re-found by its identity within your scope: a target that vanished or is no longer yours is skipped (one combined count — the two are deliberately not distinguished), and one whose value (or version field) changed since the dry-run is reported as a conflict and left untouched.
- Results state committed values only, with
selected/applied/conflicted/disappearedOrUnauthorizedcounts. - Every applied change is journaled and can be undone. Undo restores the previous value only where your annotation is still the current value — it never overwrites a later edit.
- Fetched data may flow into an annotation only as a scalar captured at the selection barrier, frozen at dry-run time.

