Intermediate~18 minHazelJS
Compensating Action
Undo or neutralize partial side effects after agent or workflow failure using saga/flow compensation and idempotent reverse tools.
- Authors
- editorial-team
- Published
- Last reviewed
Problem
Multi-step agent or workflow work touches multiple systems; a mid-path failure leaves reserved inventory, charged cards, or open tickets that cannot share one DB transaction.
Context
AgentRuntime write sequences and @hazeljs/flow / @hazeljs/saga orchestrations where forward steps need explicit undo. Irreversible actions need HITL instead of silent compensate.
Forces and constraints
- Distributed systems rarely offer 2PC across SaaS APIs
- Compensation must itself be idempotent
- Not every action is compensatable (data deletion, public posts)
- Operators need a clear compensated vs failed state
Recommended design
Model forward writes with paired compensating @Tool methods (or @SagaStep compensate handlers / flow compensate nodes). On failure, run compensations in reverse order under the same policy/auth. Prefer compensate-by-key using the original idempotency/natural key. If compensation is impossible, escalate to HITL and mark the AgentRun for manual reconciliation—do not invent destructive 'undo'.
Minimal pseudocode
// Saga-style (cross-module) — @hazeljs/saga
@Saga({ name: 'create-order' })
class OrderSaga {
@SagaStep({ order: 1, compensate: 'cancelReservation' })
async reserveInventory(ctx) { /* ... */ }
@SagaStep({ order: 2, compensate: 'refundPayment' })
async processPayment(ctx) { /* ... */ }
async cancelReservation(ctx) { /* idempotent cancel by reservation id */ }
async refundPayment(ctx) { /* idempotent refund by payment idempotency key */ }
}
// Agent tools: expose compensate_* tools only to recovery paths / PolicyEngine
@Tool({ name: 'compensate_refund' })
async compensateRefund({ refundId, idempotencyKey }: { refundId: string; idempotencyKey: string }) {
return payments.voidOrRefund({ refundId, idempotencyKey });
}Failure modes
- Compensation that is not idempotent doubles undo damage
- Compensating irreversible actions (pretending delete is undoable)
- Forward path succeeds after compensation started (race)
- No audit of compensated runs
Security considerations
- Authorize compensate tools as strictly as forward writes
- Do not expose compensate_* to open agent loops without policy
- Redact PII in compensation logs
Observability signals
- State: completed | failed | compensating | compensated
- Link forward executionId to compensation run
- Alert on compensation failures (manual queue)
Evaluation approach
Inject failure after step N; assert compensations fired once; verify external systems match expected neutralized state.
Trade-offs
- Saga complexity vs accepting manual ops for rare failures
- Compensation lag vs holding stronger locks (often unavailable)