01

Approval belongs to a version, not only to a record

A CRM agent reads an opportunity, drafts a stage change, and waits for approval. While it waits, a person corrects the amount, a connector records a reply, or another workflow changes the owner. If the eventual write names only the record ID, it can succeed against a state nobody reviewed. The mutation may be well formed, authenticated, and still wrong because its evidence has expired.

Give every mutable relationship resource an opaque version generated by the server. The read response returns that version; the proposal carries it unchanged; the approval names the proposal and observed version; and the write is accepted only if the current version still matches. The token is evidence about state, not a timestamp the agent invents and not a summary of customer content. A record ID answers where to write. A version answers which state the decision was about.

RFC 6585 defines HTTP 428 Precondition Required for an origin server that requires a conditional request, with avoiding lost updates as its typical use. That is a useful protocol precedent for refusing an unversioned write. The status code is optional, however, and the RFC does not define CRM authorization, agent approval, or a complete concurrency policy. An open CRM can use HTTP validators, an explicit API field, or another storage-neutral mechanism as long as the condition is mandatory and enforced at the write boundary.

  • Name the workspace, stable resource ID, operation, intended field changes, and server-issued observed version.
  • Bind approval to a digest of that proposal so a later tool call cannot expand the reviewed change.
  • Keep the version opaque; do not encode names, email addresses, notes, or other relationship content into it.
  • Require the condition for human interfaces, agents, imports, connectors, and background jobs that share the same record.
02

A conflict is fresh work, not permission to retry

Immediately before mutation, the server should re-check actor identity, workspace, grant, policy, approval, emergency-stop state, operation scope, and the resource version. The version comparison and data change need one atomic boundary. If the condition fails, change nothing. Return a typed conflict that identifies the resource, failed condition, current opaque version, and a safe next step without echoing restricted fields or customer prose.

Google AIP-134 documents an update pattern in which a resource carries a server-computed etag and a supplied etag must match for the update to succeed. It also keeps the etag check independent of the update mask. That is a useful API design precedent, not a requirement that every CRM use Google APIs or a claim that an etag grants permission. Field selection, authorization, purpose, approval, idempotency, and freshness remain separate controls even when they travel in one command.

RFC 6902 offers a narrower field-level precedent. A JSON Patch can include a test operation, and an unsuccessful operation makes the patch fail rather than count as successfully applied. This can protect an assumption such as “the stage is still proposed” inside an atomic patch. It is not a substitute for a whole-resource version when other changed fields also affect the decision, and sensitive values should not be copied into a patch merely to create a test. Use the smallest non-sensitive condition that actually proves the proposal is still valid.

Never turn version_conflict into an automatic retry of the same mutation. Refresh the permitted view, show what changed at an appropriate level, recompute the proposal, re-run policy, and obtain fresh approval when the consequence or reviewed facts differ. Idempotency answers whether the same accepted command may be safely recognized again; a version condition answers whether the command still belongs to the current state. A trustworthy tool contract needs both.

  • Return no partial record write when the precondition fails.
  • Reveal changed field names or a policy-safe comparison only when the caller is allowed to inspect them.
  • Invalidate approval when the target version, proposal digest, grant, policy, tool, recipient, or consequence changes.
  • For multi-record work, declare every dependency and commit atomically or split the workflow into separately reviewed commands.
03

Prove the stale path before an agent receives write access

Build deterministic races with fictional records. Let a person correct a phone number after an agent reads it. Let a connector close an opportunity while a stage proposal is pending. Reassign an account, revoke a grant, merge a duplicate, end a relationship role, or update communication permission between preview and execution. In every case, assert that the stale command makes no change, triggers no downstream effect, and returns a machine-readable recovery path.

Test through every supported storage path, but keep the public contract independent of a database feature. SQLite or D1 may enforce the condition with a transaction and a version column; a future PostgreSQL adapter may use a different primitive. The externally visible rule stays the same: the server issues the token, successful relevant changes advance it, and a mismatched write cannot silently win. Avoid second-resolution updated-at fields as the sole token because two changes can share a clock value and clocks do not express which fields belong to one atomic state.

Record content-minimal receipts for the proposal ID, actor and delegation reference, target and observed version, policy and approval references, tool revision, outcome, resulting version, and conflict reason. Do not log the full before-and-after customer record by default. Make the version field, conflict vocabulary, and receipt schema exportable and documented so self-hosters and alternative clients can preserve the safety contract without inheriting hidden vendor state.

The human experience should be calm: “This record changed after review” followed by a focused comparison and the option to discard or prepare a new proposal. The agent may summarize the permitted difference and recommend a next step, but it must not decide that the newer human or connector change is inconvenient and overwrite it. Open CRM is strongest when people and agents can work on the same relationships without the last writer quietly erasing the truth the other one just added.

  • Exercise same-field, different-field, related-record, permission, and approval races with synthetic actors.
  • Verify that queues, retries, offline clients, imports, and connector callbacks preserve the original condition.
  • Confirm that a conflict receipt contains enough metadata to investigate without becoming a shadow customer record.
  • Round-trip versioned commands and receipts through documented export fixtures without treating an old token as current authority.