ACAgentic Craft

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)

Sources

Related patterns