Summary

  • ifUnchangedBy lets a JMAP client protect a write with conditions on selected properties of selected objects, avoiding conflicts caused by unrelated changes to the same data type.
  • atomic:true adds a different guarantee: every requested change in one Foo/set commits together or none does. Condition success and atomic commit still do not prove human authority, external propagation or user outcome.

The account was busy enough that its state string changed constantly. Mail arrived, another device set a flag, and a server-side process updated an object. A desktop client had read file f42, prepared new content and then submitted a guarded update. Core JMAP's ifInState compared the state of the entire object type. The token no longer matched, so the server refused the method even though f42 still held the exact content on which the edit depended.

Removing the guard would have made the request quieter and the evidence worse. Retrying blindly would turn a known conflict into an unknown overwrite. What the client actually needed was narrower: replace the content only if f42 still referenced blob G_old.

That is the problem addressed by JMAP Conditional Set. Revision 00 is dated 15 September 2026 and expires on 19 March 2027. At the research cut-off it was an active JMAP Working Group Internet-Draft intended for Standards Track and proposed to update RFC 8620 if approved. It is working material, not a final RFC, implementation certificate or deployment measurement.

The draft adds ifUnchangedBy to Foo/set. Each entry maps an object ID to a PatchObject describing values the client expects. The server asks whether applying that patch to the current object would change anything. If every selected pointer already has the expected value, the condition holds. A JSON null asserts absence. No new version field is invented; the mechanism reuses the representation and patch semantics JMAP already defines.

That precision changes what can be rejected. It also changes what can be claimed.

A selected fact is not a version of the whole object

Suppose the condition names only blobId. If the file's name changes while its content reference remains G_old, the content replacement may proceed. That is the feature. The client declared that the content identity mattered and the unrelated name did not.

The successful response therefore cannot honestly be summarized as “the object was unchanged”. It proves the narrower proposition: at the method's evaluation point, the readable pointers named by the condition matched their asserted values. Unselected properties may have changed. The server may have generated other state. An external policy may have changed while the object stayed identical.

Whole-object compare-and-swap is possible only when the data type supplies a trustworthy server-maintained property that changes on every modification. A changed timestamp can serve if its update discipline is complete. If some write path fails to advance it, the condition becomes a polished stale-state receipt. Token governance—not the presence of condition syntax—determines the strength of the claim.

The distinction matters because evidence systems prefer one green label. “Precondition passed” is easy to store. The named pointers, comparison representation, object visibility and evaluation time are harder. Dropping that context causes the receipt to grow powers it never had.

The condition can reach beyond the object being changed

Conditions are evaluated at the start of the method, before any creation, update or destruction inside it. They can name another object of the same type that the method does not modify. A file update can depend on the file still having parent d3 and on d3 still being named Reports. If either proposition fails, the server rejects the whole method with stateMismatch and makes no change.

This is stronger than protecting only the target's content pointer. It can bind a limited piece of context into the write decision. It still is not a policy graph.

The condition says nothing about an ancestor not named in the request, a mount outside the JMAP object model, an organizational hold, or the principal who is entitled to decide that a report belongs there. Selected context reduces one race. It does not authorize every consequence attached to that context.

The draft also permits conditions on readable server-set properties. That is useful: a client can depend on a size, content identifier or maintained change time it cannot itself edit. But read visibility is a real boundary. The server must not evaluate a condition on a property the requester cannot read. It must fail with forbidden, otherwise binary condition results could become an oracle for hidden values.

Permission to test a fact remains separate from permission to perform the proposed mutation. The method can still fail for authorization. A matching value is evidence about state, not a power of attorney.

One method, one conditional outcome

If any ifUnchangedBy entry fails, the whole Foo/set makes no creation, update or destruction. The server returns method-level stateMismatch; the state string remains unchanged. A server may identify failed IDs but does not return their current values.

That all-or-nothing effect is already part of conditional evaluation. A client wanting independent results must send separate method calls, each with its own condition. This makes the unit of decision explicit. Combining ten changes behind one condition means accepting that one mismatch prevents all ten. Splitting them means accepting independently visible transitions.

Neither shape is universally correct. The choice belongs to the application that understands the business invariant. A protocol can expose the control without deciding which assets must rise and fall together.

This is where a superficially similar word—atomic—needs discipline. A condition decides whether the method may begin from the asserted state. Atomicity decides whether the method's own set of requested changes can commit as one unit. They answer different questions and have different error receipts.

Atomicity protects the proposed final set

Core JMAP normally processes creations, updates and destructions independently. Some may succeed while others fail. The draft's optional atomic:true changes that contract. Every requested change in the method takes effect, or none does.

Constraints are evaluated against the proposed final object set rather than against intermediate order. Two sibling file nodes can exchange the names current.txt and previous.txt. Either rename alone collides with the name the other object still holds. Evaluated together, the final state remains unique. Atomic evaluation allows the valid whole to exist without inventing a temporary name.

If any constituent operation fails—a malformed patch, permission error, uniqueness violation or another SetError—the server returns atomicFailure and commits nothing. It may identify the failing IDs. IDs not listed still did not commit; omission means they were not the cause, not that their changes took effect.

If a server advertising the capability cannot apply a particular method atomically, it must return cannotApplyAtomically and make no change. It must not silently fall back to partial application. That refusal is one of the draft's most important operational properties. It preserves the difference between “the requested guarantee was unavailable” and “some useful work happened anyway”.

A client may retry without atomicity if partial application is acceptable. But the weaker retry is a new decision. It needs its own reason, audit record and recovery plan. Treating it as an automatic transport retry would erase the application's invariant at the exact moment the server said it could not enforce it.

Commit is not propagation

An atomic JMAP response establishes a boundary inside the responding server's Foo/set. It does not make search indexes, notification queues, delivery workers, remote replicas, caches or user interfaces part of the same transaction.

After an atomic name exchange, a path cache may still show the old mapping. After a conditional sharing change, an already issued capability may remain valid until another subsystem processes revocation. After a conditional email destruction, an offline device may still display a local copy. These are not contradictions in the method receipt. They are effects outside its declared scope.

Leadership systems get into trouble when they name a database transition after the desired business result. “Access revoked”, “file replaced” and “message destroyed” can each be true at one layer and false at another. The defensible record identifies the account, object IDs, selected pointers, initial-state evidence, method call, response, committed state, external observers and final outcome separately.

Heng Lu's thin-layer discipline is useful here. A coordination mechanism earns legitimacy by solving a bounded problem and preserving the boundary. ifUnchangedBy can prevent stale selected facts from governing a write. atomic:true can prevent partial method commits. Neither should be promoted into authority over people, external systems or consequences it never evaluated.

Test the failure shapes, not only the green path

Running code should demonstrate the negative claims. Change an unrelated object and confirm a selected-object condition still succeeds while ifInState would fail. Change an unselected property on the target and confirm the narrow condition remains true. Change the selected property and confirm the entire method leaves state untouched.

Attempt a condition on a hidden property and require forbidden, not a Boolean clue. Move the parent while leaving the blob unchanged. Swap two unique names with atomicity, then repeat against a server that cannot guarantee the unit. Force one constituent authorization failure and verify that none of the otherwise valid changes appears.

Finally, delay every external consumer after a successful commit. The UI, index and worker should reveal which receipt each can actually produce. A good implementation makes a partial evidence chain visible. A dangerous one paints the first green response across the entire system.

The stale state token and the unchanged object were both real. The new draft helps the client say which reality matters. That precision is valuable only if the resulting receipt stays as narrow as the condition that created it.

Sources