Not a valid ISO-8601 duration. Expected e.g. PT5M, PT1H, P1D.
A save can fail four ways, and the correct user action differs every time. Collapsing them into one "Save failed, try again" toast is what turns a recoverable conflict into silent data loss.
The resolved view moved between the read and the write — someone else edited, or an
ancestor changed (the version tracks inherited changes too). The server refused rather than
overwrote. Offering "Retry" here would resend expectedVersion stale, and the only way
to make a blind retry succeed is to drop expectedVersion — which silently destroys the
other edit. So the UI re-reads and asks per key.
| Key | Now on server | Your change | Keep |
|---|---|---|---|
| notifications.digest.frequency changed by j.okafor · 2 min ago · "aligning with EU digest policy" | weekly | daily | |
| notifications.retry.backoff changed at portal Acme Portal — inherited change, not a peer edit | PT10M | PT5M |
There is deliberately no "Force save" / "Overwrite anyway" affordance. The only way to take your value is to see the other value and choose it.
The batch is atomic: one invalid value means nothing was written. The message names what was allowed, so the user learns the rule rather than guessing. Errors render against their own inputs — a summary banner alone makes the user hunt.
Not a valid ISO-8601 duration. Expected e.g. PT5M, PT1H, P1D.
Not one of the permitted values: hourly, daily, weekly.
The editor disabled nothing, because at read time the key was not locked — an ancestor locked it while the page was open. Proof that the disabled input is a courtesy and the server is the boundary. The row becomes locked in place and names the new lock owner.
🔒 portal Acme Portal locked this value. lockReason: "SMS is disabled tenant-wide pending carrier review."
The key's schema changed after the value was written. The read
succeeds — it returns the default and explains why in warning, rather than
failing, because one stale value must not break a consumer's boot. The editor must show both
numbers: the value in force now, and the orphaned stored value the user last chose. Showing only the
default makes their setting look silently discarded.
Contract. PUT /v1/config with expectedVersion → VERSION_CONFLICT
when the resolved view moved; ConfigWriteResult.version is the next
expectedVersion. Errors branch on ErrorBody.code
(ErrorCode), never on message — the message is human-facing and may change
without a contract version bump. warning on
EffectiveConfigEntry drives block D.
Atomicity is a UX property, not just a DB one. Every failure banner says explicitly that
nothing was saved, because "some of your changes were applied" is the state users cannot reason about.