What If a Document Changes After Approval?

Close the gap between checking approval and committing a document action. Test competing edits, AI patches, delayed workers and uncertain external outcomes.

A correct check can still precede an incorrect action

Checking that a document is approved does not make the next write safe. Another user or AI worker can change the document between the check and the action. The protection must bind the state transition to the package and authority that were evaluated, and every relevant writer must participate in that contract.

Consider a hypothetical operations workflow publishing a service notice. Notice N20, revision R4, has been approved for a named audience. A release worker reads R4 and verifies the grant. While that worker prepares its request, an editor saves R5 with a different outage window. If the worker then publishes whatever the document identifier currently returns, it can release R5 under R4's approval.

Reading twice narrows the interval but does not close it. Another edit can arrive after the second read. The engineering question is which state the system is allowed to commit when the expected revision or grant no longer matches. The historical approval of R4 can remain valid while publication of current R5 is held for review.

This article proposes a concurrency review using synthetic notices and an inert publication adapter. It is not a customer incident, a legal approval standard or a guarantee for every document platform. The workflow owner must choose whether an approved historical package may be published, whether only the current approved package may be used, and how edits interact with already committed release work. Those are different policies, not interchangeable implementations.

Define what the transition must protect

Name the action and its invariant before choosing a lock or isolation level. In the notice example, suppose the owner requires that a release intent refer to the current approved package at the local commit boundary. Bind the notice scope, immutable package revision, approval grant, permitted audience and action to that transition. Include attachments and significant structured fields if they can change independently.

An invariant about the local release intent is narrower than a promise that every external recipient always sees the latest approved notice. An edit can follow the local commit, a dispatch can be delayed and a recipient can receive an earlier message. Describe those boundaries directly. If the business requires stricter external behavior, identify the destination capability or controlled release process that can enforce it, rather than treating a database check as a global guarantee.

Define what a conflict means for the operator. A newer package requires a package review; a revoked grant requires an authority decision; a completed release requires receipt investigation rather than another release. They should not all return a generic retry message. Preserve the losing request's expected revision and reason so the user can understand the hold without reconstructing a race from logs.

Decide whether a release stage freezes the selected package, allows parallel creation of a newer draft, or accepts edits that can invalidate pending work. Freezing one approved package does not have to prevent all drafting. It does require the release path to read the identified immutable representation, not a mutable current-file link that can later resolve to different content.

Make the local decision and intent one protected transition

Inside a single database boundary, combine the expected package and grant checks with the state change and its identified release intent. A conditional update can reject a mismatched revision; locking or an appropriate isolation design can coordinate dependent records. The correct choice depends on where the invariant lives, how attachments and grants are stored, and which competing operations can affect it.

PostgreSQL's isolation documentation explains that ordinary Read Committed queries use statement snapshots and successive queries can observe different committed states. It describes update behavior under concurrent changes and the need to retry whole transactions after relevant serialization failures. Merely wrapping a read and a later write in a transaction does not establish every business invariant.

Make the application interpret the actual outcome. An update affecting no matching row is not a successful release. A transaction that rolls back must leave no dispatchable intent. If approval and attachments live in separate records, protect their relationship as well; checking one document row cannot prove that an independent attachment or grant remained unchanged.

Ensure that manual editors, imports, AI patch workers and administrative repair paths respect the same protected state. A carefully implemented release worker cannot compensate for another writer that silently replaces package content without advancing the authoritative revision. Do not rely on frontend controls to stop that path. The service owning the state must enforce its transitions.

When a retry is permitted, restart the decision with current authoritative state and the original request identity. Do not replay a cached conclusion that R4 remains current. A bounded retry may discover that R5 now needs review; that is a valid hold, not a database error to conceal. Keep external publication outside speculative transaction attempts so retrying a local transaction cannot repeat an external effect.

Treat conditional APIs as resource-scoped protection

For an external editable resource, examine the destination's supported version precondition. RFC 9110's If-Match semantics define strong entity-tag comparison at the origin server before applying the method. They describe conditional requests as protection against lost updates. The wildcard tests resource existence, not equality with a particular reviewed version.

Where the actual API supports this contract, carry the expected resource validator into the state-changing request and preserve the response. A preceding GET followed by an unconditional write does not provide the same protection. A client-side header that an integration drops or an endpoint does not enforce cannot establish the precondition merely because it appears in a request log.

Establish what the validator covers. A document representation's ETag does not automatically cover a separate attachment, approval grant, tenant policy or recipient list. The API might expose a package-level condition, or the application may need a separately governed immutable package. Do not claim cross-resource approval integrity from one resource's validator.

If the precondition fails, stop the old action and surface the actual conflict. Fetching the new version and retrying with its tag would change which content is acted on. That requires a newly evaluated request and any necessary approval; it is not a transparent transport retry. Likewise, replacing an exact validator with a wildcard removes the intended version protection.

If the API cannot enforce the required condition, record that limitation. A narrower permitted action, a controlled publish window or a destination-specific release capability may be acceptable to the owner. If none can uphold the required invariant, keep the action held. Adding faster polling or a more confident AI summary does not eliminate the residual race.

Keep AI edits and delayed workers attached to their base

An AI editing task should identify the base package it used, the proposed patch and the allowed mutation scope. If R5 is current when a patch based on R4 arrives, do not overwrite R5 with a regenerated full document. Hold the patch for an explicit merge or regenerate it against the new base, with its differences available for review.

Merging text successfully does not carry approval into the merged package. A technically conflict-free merge can change a significant condition or combine attachments that no approver reviewed together. Treat the resulting package according to the workflow's significance rules. The AI may prepare a comparison, but the authoritative stage must decide whether the new package is usable.

Delayed release workers must load the identified intent and package, not infer authority from a conversation saying Approved. Before dispatch, apply the declared policy for expiry, revocation and changes after local commit. Record whether the worker is executing a pinned historical release or a current-package release. Those choices should be visible to the operator and supported by the actual retained representation.

If edits invalidate pending work, the editor and dispatch path need a coordinated cancellation or reservation protocol. Marking an intent canceled while a worker is already sending is not proof that the effect was prevented. Classify an already issued or uncertain request separately and reconcile its receipt. An owner can permit corrective publication, but that is a new scoped operation rather than deletion of the earlier evidence.

Test the race at deliberate interruption points

Use controlled barriers, not just a fast repeated test that happens to finish correctly. Pause the release worker after its approval read, commit an editor's R5 change and then resume the worker. The expected result must follow the declared invariant: no intent for an unapproved R5, and either a permitted pinned R4 operation or an explicit hold under the selected policy.

Separately race attachment replacement, grant withdrawal and an audience change against the commit. These cases reveal whether the protected boundary actually includes all significant records. Use a deliberately permissive candidate that reads approval and later publishes the current file. The independent observer should demonstrate that the test detects this failure condition before trusting a passing hardened candidate.

Run two release workers against one intent. Verify the number and identity of destination effects through the inert adapter's ledger, not only a local completed flag. Restart after local commit but before dispatch, and after the adapter accepts the request but its response is withheld. An uncertain external result must retain the original operation and its attempted package for reconciliation.

Also simulate a lost response from the local commit. An application timeout is not proof that the transaction failed. Read back the scoped request and intent before creating another release. An unavailable or incomplete readback leaves the outcome unknown; it is not permission to mint a new request identity. Keep database retry, delivery retry and newly authorized publication as different operations.

Export the expected revision, resulting revision, grant reference, transition outcome, intent identity and adapter evidence for each schedule. Retain competing requests that were rejected as well as the winner. Passing one arrival order is insufficient when the policy must survive both edit-first and release-first schedules. Document which boundary was tested and what remains unverified at the real destination.

Use an interruption worksheet before widening the workflow

The worksheet below describes the proposed current-package release policy for the synthetic notice. It is an acceptance artifact, not evidence that any production service already implements these controls. Adapt the expected result when the owner deliberately permits historical publication, and keep that exception explicit.

| Interruption point | Competing change to inject | Evidence required before proceeding | | --- | --- | --- | | After approval read | Commit a newer significant package revision | Old request cannot authorize the new package | | During local commit | Replace an attachment or withdraw the grant | Protected transition rejects or re-evaluates the changed state | | After intent commit | Change the notice before delayed dispatch | Declared pinned-release or invalidation policy is enforced | | During destination write | Change the resource protected by its validator | Exact destination precondition is honored, without wildcard fallback | | After acceptance, before response | Withhold a local or destination receipt | Original operation is reconciled without a new effect identity |

Assign the next action for every conflict or unknown result. A version conflict may need renewed review; a receipt gap needs investigation of the original operation. Do not present both as a button that blindly retries the write. The operator view should identify the affected package, the enforced boundary and the next responsible owner.

For the next step, complete this worksheet for one release action and run both edit-first and release-first fixtures in isolation. Use the document-version review for readable package binding and the write-recovery playbook for uncertain effects. Bring one scoped concurrency record to a backend systems review or a system architecture review. Reading and resource downloads require no email address; requesting contact remains optional.