Compensation

Configure reversible Steps and choose whether failure rolls back one Step or the full Flow.

Configure a Compensated Step

Set handling to compensated, configure the compensation endpoint, and select the Attributes it receives:

{
  "id": "charge-card",
  "type": "service",
  "handling": "compensated",
  "http": {
    "invoke": {
      "endpoint": "https://payment.example.com/charge",
      "timeout": 1000,
      "mode": "sync"
    },
    "compensate": {
      "method": "DELETE",
      "endpoint": "https://payment.example.com/charges/{charge_id}",
      "timeout": 3000,
      "mode": "async"
    }
  },
  "attributes": {
    "amount": {"role": "required", "type": "number"},
    "charge_id": {
      "role": "output",
      "type": "string",
      "compensated": true
    }
  }
}

compensated: true selects the data available to the endpoint, making the compensation contract and its data exposure explicit. Each selected Attribute uses its invocation name. When an input and output share that name, select the value the compensation operation consumes.

The compensation method defaults to POST. Its timeout defaults to the invocation timeout when omitted. Invocation and compensation modes are independent, and each defaults to sync.

Request Contract

POST and PUT send the selected Attributes as one JSON object:

{
  "charge_id": "ch_abc123"
}

GET and DELETE resolve selected values into endpoint placeholders. Every method receives Argyll-Flow-ID, Argyll-Step-ID, and Argyll-Receipt-Token; use the receipt token as the endpoint’s idempotency key.

Async Compensation

Set http.compensate.mode to async when rollback continues after the initial compensation request:

  1. Argyll sends the compensation request with an action-specific Argyll-Webhook-URL.
  2. The service returns 2xx after accepting the rollback. This does not complete compensation.
  3. The worker posts to the supplied URL when rollback finishes. An empty callback body reports success; application/problem+json reports permanent failure.

The compensation callback URL ends in /compensate; the invocation callback ends in /invoke. Post to the header value rather than constructing it. Repeating a settled callback is safe.

A non-2xx response to the initial request follows normal retry rules: 5xx and transport failures are retryable, while 4xx is permanent.

Choose the Rollback Scope

When a compensated Step fails, Argyll compensates its succeeded Work Items. This gives a for_each Step a consistent cleanup path when some items succeeded before another exhausted its retries.

Enable Rollback on Failure when starting a Flow to compensate every succeeded Work Item in that failed Flow whose Step uses compensated handling:

{
  "id": "checkout-123",
  "goals": ["ship-order"],
  "compensate": true,
  "init": {
    "order_id": ["ord-abc"]
  }
}

Flow Steps expose the same setting as flow.compensate, so each child Flow carries its own rollback policy.

Ordering

Flow rollback follows reverse dependency order. Steps that ran in parallel compensate in parallel; the preceding dependency wave starts after the current wave settles.

graph TD Reserve["reserve-inventory
third wave"] Address["validate-address
third wave"] Charge["charge-card
second wave"] Ship["ship-order
first wave"] Reserve --> Charge Address --> Charge Charge --> Ship

Work Items within one Step compensate independently. A succeeded Work Item that finishes after Flow failure joins the appropriate rollback wave. The Flow deactivates after all active and compensating work settles.

Child Flows keep their own rollback boundary. A Flow Step’s flow.compensate setting controls its child, while the parent controls compensation of the Flow Step itself.

Retries and Recovery

Compensation uses the Step’s work_config retry and backoff settings. Retryable failures resume after engine restart, and permanent or exhausted failures finish as compensation_failed. See Retries for configuration and WebSocket API for event names.