Compensation is another side effect
Z29C stopped calling withdrawal and release “rollback” and gave every counteraction its own authority, identity, receipts, uncertainty, and visible limits.
Z29C delivered a synthetic document, failed at a later step, and “rolled back” the delivery.
The final workflow appeared clean. The destination history showed that the document had existed for seven minutes. A notification stub had already recorded that a recipient was told about it. Withdrawal prevented future access and did not make the earlier events un-happen.
The word rollback had smuggled a database promise across several independent systems.
I renamed the operation compensation and treated it as another side effect. It needed its own intent, authority, request identity, attempts, receipt, unknown-outcome state, and limitations.
The original effect remained part of the workflow history.
Database rollback and workflow compensation were different
Inside one PostgreSQL transaction, rollback could make uncommitted local changes invisible. No external observer should have seen them as committed state.
Z29C's workflows crossed boundaries. A reserved resource could influence capacity. A delivered document could be opened. A notification could be read. A generated artifact could enter an immutable store. Later actions could depend on those effects.
A compensating action changed the world again:
- Release the reserved resource.
- Withdraw the document.
- Send a correction.
- Revoke future access.
- Mark a prior result superseded.
None restored every observer to an exact earlier state.
I reserved rollback for local transactional behavior and used compensation for external counteractions. The vocabulary forced the final state to retain history and limits.
Words did not change mechanics by themselves. They stopped the interface and design notes from claiming reversibility the adapters did not provide.
Compensation began with a new durable intent
The first engine invoked a compensation callback inside the failure handler. If the process crashed, Z29C could not tell whether compensation had started or finished.
The revised workflow created a durable compensation step linked to:
- Original workflow and effect step.
- Original effect receipt and external reference.
- Failure or cancellation condition requesting compensation.
- Compensation policy version.
- Stable compensation request identity.
- Current authority decision.
The step entered the same execution protocol as forward work. A worker leased it, persisted request identity, invoked the adapter, and committed a typed receipt.
Repeating the failure handler found the existing compensation intent rather than creating another. A browser cancellation repeated with the same key returned the same recovery path.
Counteraction became resumable even if every worker process disappeared.
The workflow could now say withdrawal requested, withdrawal verified, or withdrawal status unknown instead of collapsing all three into rolled back.
Preconditions came from the original receipt
Compensation needed to target the exact effect it intended to counteract.
The original delivery receipt supplied document digest, destination reference, adapter contract version, and accepted time. Withdrawal used that reference. It did not search by a mutable title or regenerate a payload from current workflow input.
The adapter checked:
- Does this external result exist?
- Is it still eligible for withdrawal?
- Has it already been withdrawn under this compensation key?
- Has another action superseded it?
- Does current policy authorize this counteraction?
If the original step was outcome unknown, Z29C did not invent a reference. It queried or reviewed before compensating where possible. An adapter operation such as “ensure reference X is absent” could tolerate absence, but only if the destination truly supported that idempotent semantic.
Compensation depended on evidence of what had happened, not merely the fact that a later step failed.
The receipt connected the two actions without pretending they formed one transaction.
Automatic compensation was not always the right response
The first saga-like definition compensated every completed prior step after any terminal failure. A scenario reserved a scarce synthetic resource, prepared a document, then encountered a temporary delivery delay. Immediate release made a safe retry impossible and defeated the original user intent.
Compensation policy considered:
- Is the failure terminal, retryable, unknown, or beyond the workflow deadline?
- Does the completed effect continue creating harm or cost while retained?
- Can forward recovery still satisfy the original intent?
- Will compensation destroy evidence needed for reconciliation?
- Does a person need to choose between waiting and counteracting?
- Can the compensation itself be performed safely now?
A temporary rate limit scheduled waiting. An unknown delivery paused before withdrawal. A terminal validation failure after reservation could release automatically if the adapter guaranteed idempotent release. A user cancellation after delivery requested review of withdrawal limits.
The engine stopped treating compensation as a cleanup reflex. It became a domain decision about the best reachable state.
Reverse order was a starting heuristic
Compensation is often described as undoing completed steps in reverse order. That can respect dependencies and is not universally sufficient.
Consider:
reserve resource
generate document
deliver document
send notificationIf notification completed and a later archival step failed, sending another notification “undo” before withdrawing the document might confuse recipients. Withdrawing first could make the correction link fail. Releasing the resource might be independent of both.
Z29C definitions declared a compensation graph rather than reversing the forward list mechanically. Edges represented safety and communication dependencies. Some counteractions could run in parallel; others required a verified prior receipt.
The graph also included non-compensable effects. A read notification could not be unread. The best response was a correction or visible supersession.
Reverse order remained a useful default for nested resource acquisition. The workflow had to defend it under actual external semantics.
Compensation could become outcome unknown
The withdrawal stub accepted a request and lost its response. Z29C's first compensation handler retried and attempted withdrawal twice.
That was less harmful than duplicate delivery in the synthetic adapter and exposed the same protocol flaw.
Compensation used a stable key and an adapter receipt. If response loss occurred, the step entered outcome unknown. Recovery queried withdrawal status or retried the same key when safe.
Dependent compensation steps waited for evidence. If release of a resource depended on the document no longer being available, an unverified withdrawal could not satisfy the edge.
The final workflow could remain recovery incomplete even though forward work had stopped. A compensation deadline and review path prevented indefinite invisible cleanup.
Treating the counteraction as special “undo” code would have hidden its ordinary distributed failure modes.
Every external effect has a boundary. Intention to restore does not make that boundary safer.
Idempotent compensation needed semantic care
An endpoint could accept the same withdrawal key twice and return one result. That made request repetition safe. The operation's meaning still needed clarity.
“Toggle active state” was not idempotent. Repeating could reactivate. “Set result X to withdrawn if currently active” was safer. “Ensure X is inaccessible” was stronger but might hide a later deliberate republication under the same reference.
Z29C used explicit target revision and expected state:
target external reference x-82
expected current state delivered
requested state withdrawn
compensation identity c-11If the destination showed a newer superseding state, it returned conflict rather than overwriting it. A second identical compensation returned the first receipt.
Set-state semantics and compare conditions made idempotency meaningful. The key alone did not turn a toggle into a safe protocol.
The adapter contract scenarios repeated, reordered, and conflicted compensation requests.
The original workflow may have been authorized to deliver. That did not imply any future worker could withdraw indefinitely.
Before compensation invocation, Z29C requested a short-lived capability scoped to adapter, target reference, action, workflow, and compensation step. Current policy could require review or deny the action.
An emergency stop policy could authorize containment while ordinary withdrawal required the initiating subject. The exact model stayed small in the synthetic environment and made the distinction visible.
The compensation receipt named the policy decision and version without storing credentials. If authority changed during waiting, the step revalidated at execution.
A denied counteraction did not erase the original effect or mark the workflow recovered. It became a visible recovery condition with a reason.
Rollback language had hidden authority because local code looked entitled to reverse its own work. External consequences belonged to current policy, not authorial ownership of the workflow definition.
The original Cancel button promised one outcome across every workflow state. After partial completion, that promise was false.
Z29C projected available actions:
- Cancel before delivery: prevents remaining effect; no compensation required.
- Stop remaining work: preserves completed effects and prevents later steps.
- Request withdrawal: creates a compensating effect with stated limits.
- Send correction: adds a new effect because the prior message cannot be retracted.
- Review unknown status: establishes what exists before counteracting.
The confirmation view listed completed milestones and the exact proposed counteractions. It did not use “undo everything.”
After compensation, the timeline displayed both events:
Document delivered at 14:03. Withdrawal verified at 14:10. The earlier notification may have been observed.
This gave the initiating person a truthful expectation. It also prevented a clean green end state from erasing the incident-like recovery path.
Manual compensation received receipts too
Sometimes a person acted in the synthetic external interface because Z29C's adapter lacked the required command. The first version let the reviewer check a box saying cleanup complete.
Manual resolution recorded target, observed external state, action performed, result reference, author, time, evidence, and limitations. If the external action had no stable ID, the receipt said so.
Z29C did not claim it had executed the step automatically. The workflow accepted a manual compensation receipt because the evidence contract allowed it.
If later automation discovered a conflicting state, the recovery reopened. The manual record remained as an observation and action, not infallible truth.
This matched W93H's separation of observation, action, and hypothesis. A person could repair the system without forcing the history into a binary checkbox.
Manual work entered the same durable narrative as worker work.
A failing workflow could create several compensating steps, each with retries and external costs. Automatic recovery could consume more calls than the original intent.
Z29C counted compensation attempts separately and within the total workflow budget. Definitions named maximum elapsed recovery time, external-call count, and when human review should replace automation.
A rate-limited compensation honored Retry-After and released workers. If its next safe attempt crossed the recovery deadline, the console surfaced the decision rather than waiting silently.
Budgets did not make an incomplete cleanup acceptable by themselves. They prevented an unbounded engine from pursuing it forever. Crossing a budget produced a visible unresolved condition and preserved safe manual paths.
The interface distinguished requested outcome incomplete from recovery incomplete. A workflow could have failed forward and still need consequential counteractions.
Cleanup consumed authority and capacity. It deserved planning rather than an infinite retry loop.
New forward work could race with compensation
A person might create a corrected workflow while the earlier one was withdrawing a resource. If both targeted the same synthetic reference, the new work could restore what compensation intended to remove or lose its dependency.
Z29C used domain references and compare conditions at the destination. The replacement workflow linked the earlier one and declared whether it depended on compensation completion.
Where a new delivery superseded the old document, the destination could accept it under a new immutable reference, avoiding a race over one mutable object. The current pointer changed through a separate operation with expected prior state.
The scheduler did not use a broad lock across workflows as the only protection. External invariants defended the shared resource, and Z29C's relation graph made intended ordering visible.
This was another reason compensation could not be an invisible callback. Other work needed to see and depend on its state.
Recovery was part of the product's current world, not a private cleanup thread.
Observability kept the original and counteraction separate
Z29C emitted semantic events:
- Compensation requested and why.
- Current policy authorized or denied it.
- Attempt and external request identity created.
- Adapter observation received.
- Compensation receipt committed.
- Residual limitation accepted.
Metrics tracked compensation age, outcome unknown, repeated attempts, manual resolution, and workflows whose forward work ended while recovery remained open.
I avoided a net-success metric that subtracted compensation from original effects. One delivery plus one withdrawal was not zero events. It was a partially completed workflow followed by recovery.
The timeline let a later reader see whether the compensation came from user cancellation, terminal failure, policy change, or duplicate-effect repair.
W93H had already taught me that a green current graph does not erase the path to it. Z29C applied the same principle to counteracted side effects.
The test harness made rollback claims fail
Scenarios asserted the limits:
- Notification is observed before document withdrawal.
- Withdrawal response is lost after acceptance.
- Original effect is unknown when compensation policy fires.
- Compensation is rate-limited beyond workflow deadline.
- Authority is revoked before counteraction.
- New workflow supersedes the target during compensation.
- Manual repair conflicts with a late automated response.
The final state had to retain original and compensation receipts, enforce ordering, and expose residual consequences. A test failed if the projection said “rolled back” or returned to a pristine pre-workflow state.
For local database-only steps inside an uncommitted transaction, true rollback remained possible and did not create compensation history. The scenarios preserved that distinction too.
The harness taught the language through invariants. If an observer could have seen the first action, history could not claim it had never happened.
Recovery stopped when counteraction became worse
My first definition kept compensating until every reversible step reported a clean result. One scenario showed why that goal was unsafe. A synthetic resource had already been used to generate an immutable document. Releasing it would save capacity but remove the reference needed to verify a disputed output. The cleanup objective conflicted with evidence preservation.
Z29C allowed a compensation plan to stop deliberately. The decision recorded which effect remained, why another counteraction would create more risk or destroy evidence, who made the decision, and which review trigger might reopen it.
This was not a generic “ignore error” path. The recovery console compared reachable states:
- Keep the resource reserved until the evidence review ends.
- Release now and retain a verified snapshot of required metadata.
- Extend the reservation within a bounded cost budget.
- Abandon the document and release, accepting loss of the disputed output.
The workflow definition could supply safe defaults. A person chose when consequence depended on context the graph did not encode.
Stopping also mattered after compensation failure. Repeated withdrawal attempts might notify an external system, consume quota, or approach an idempotency-window limit. Crossing the recovery budget moved to review instead of creating an unending “cleanup” process.
The final state could be partially recovered with accepted residual effect. That phrase was less satisfying than success and more useful than a workflow that never closed or kept acting after its actions stopped helping.
Compensation was not morally required because a forward step failed. Its purpose was to improve the reachable state under current evidence and authority. When it could not, restraint became part of recovery.
Compensation aimed for a better reachable state
The corrected model did not promise restoration. It promised a deliberate movement from the current, partially completed state toward a safer or more useful one.
Sometimes that meant withdrawal. Sometimes it meant releasing a resource, sending a correction, revoking future access, or stopping additional effects while preserving what had already happened. Sometimes no counteraction improved the situation enough to justify its risk.
Each choice remained tied to the original intent and evidence. The final workflow could explain what happened, what was counteracted, and what could not be recovered.
Calling compensation another side effect sounded less elegant than rollback. It produced the architecture needed for reality: stable identity, current authority, retries, unknown outcome, receipts, and limits.
Recovery did not erase the past. It made the next state accountable.
That state might be messier than the one before the workflow began, but its consequences were now named instead of hidden behind success.
Messiness remained visible and reviewable.