Concurrency with updated_at and If-Match
A replace without If-Match is refused, and a stale write gets the current document back.
Every document the server returns carries updated_at. It is the version stamp, and it travels twice when you replace a document:
- inside the document, unchanged from the one you read — a body whose
updated_atdiffers from the header is rejected with400; - as the
If-Matchheader, quoted:If-Match: "2026-09-01T10:00:00.000Z". A replace without it is refused with428.
If the document changed since you read it, the reply is 409 with three fields: error, current — the document as it is now, including its new updated_at — and applied, the list of stages that had already been written if the conflict surfaced part way through (normally empty). To recover, merge your change into current, keep its updated_at, and put again. Never retry with the stale document: the conflict exists because someone else's edit is in it.
A create is a PUT on the collection (/api/build/agents, /api/build/workflows) with no updated_at; it answers 201. A replace is a PUT on the item, by id or slug.
spinrun agent get triage-bot > agent.json
spinrun agent put --file agent.jsonThe CLI sends the document's updated_at as If-Match for you.