Audience: customers building integrations that mutate the same content from multiple writers (e.g., a sync pipeline + the admin UI editing the same article, or two import jobs racing on the same slug). Most customers don't need to think about this.
The fuzz battery (FUZZ-F7, 2026-06-27) flagged that parallel POST /api/import/json + DELETE /api/content/:type/:slug on a single slug is non-deterministic: the DELETE can "win" or "lose" depending on which mutation lands at the engine writer last.
This page documents the actual semantics, why they're what they are, and what to do if you need stronger guarantees.
What you get today
Each single mutation is atomic. The engine writer is single-instance + sequenced; one create/update/delete lands as one writer transaction. There's no half-state.
Across mutations on the same slug, last-writer-wins. If you fire:
T+0ms POST /api/import/json (with the slug "x" in the batch)
T+5ms DELETE /api/content/article/x
The order they arrive at the writer determines the final state. If DELETE lands first, then the import recreates the slug. If the import lands first, DELETE removes it. There's no version negotiation between the two.
This is intentional — the engine is single-writer, sequenced, and append-only. It's a coherent contract; it's just not a locking contract.
When this matters
| Scenario | Risk |
|---|---|
| One admin user editing one article | None — single source. |
| Sync pipeline + admin both touching the same slug | DELETE in admin can be silently undone by next sync tick. |
| Two import jobs racing on overlapping slugs | One job's writes overwrite the other's; no error surfaced. |
| MCP agent editing while a build is publishing | Build reads a frozen snapshot — safe. |
What to do if you need stronger guarantees
1. Idempotent imports
If your sync pipeline is the source of truth, design imports so the current state of the source is what lands — and DELETE in the admin will get clobbered on next sync. This is the cleanest model.
2. ETag / If-Match (planned, not yet shipped)
A future surface will accept an If-Match: <etag> header on PATCH/DELETE. The etag will be the content node's updatedAt. Mismatched etag returns 409 + the current updatedAt so the caller can resync. This is the standard optimistic-concurrency pattern.
Until shipped, the CMS doesn't enforce this. Track [#XXX] if it matters to you.
3. Don't race in the first place
If you have one workflow that DELETEs and another that imports, gate the import on "is the content currently deleted?" via a GET before the POST. Race window shrinks dramatically and is acceptable for most pipelines.
4. Use bulk endpoints with skipExisting: true
POST /api/import/json {records: [...], skipExisting: true} won't overwrite existing slugs — its writes use MERGE ... ON CREATE SET so a row already present is a no-op. Combined with explicit DELETE only via the admin, this prevents most race outcomes.
Why we don't lock
Locking on every slug would:
- Push contention into the engine writer (already the system bottleneck).
- Make import-throughput dependent on lock-acquisition cost.
- Surface a class of deadlock and lock-timeout errors the system is currently free of.
Sequenced last-writer-wins is the right default for an append-only content store. Stronger guarantees are layered on top (etags, opaque session tokens, leases) — and those are surfaces we'll add when customer demand justifies the engineering cost.
Audit trail still works
Even when last-writer-wins, the bitemporal audit chain records every mutation. You can ask "what did article X look like at 14:23:01" and the answer is exact, even if 14:23:02 then deleted it and 14:23:03 recreated it. The history is whole; the current state is whatever the last writer said.
Related
- API reference — endpoint surface
- Features → Replay & audit — bitemporal guarantees that are intact
- Architecture — single-writer engine model