Skip to main content
Version: 2.0 prerelease

Execution Guarantees and Idempotency

Durable Workflow v2 draws a hard line between workflow replay and activity execution:

  • Workflow code is replayed from committed history and must be deterministic.
  • Activity code performs side effects and is at-least-once.
  • The durable history layer records committed workflow and activity outcomes exactly once for a given durable identifier, even when transport delivered work more than once.

Those guarantees are what let the engine survive worker restarts, lease expiry, queue redelivery, and rolling deploys without losing the run's place.

Replay Is Not Retry

Workflow tasks rebuild state by replaying committed history and then deciding what to do next. Replay re-invokes the workflow body, but it does not re-run activities, re-send signals, or repeat side effects that were already recorded on history.

That is why workflow authoring code must stay deterministic. Use workflow-safe helpers such as Workflow::now(), sideEffect(...), queries, updates, activity results, memos, and search attributes when you need to cross the durable boundary.

Activity Execution Is At-Least-Once

Activities are the side-effecting part of the system, so the contract is different:

  • An activity attempt can be claimed more than once.
  • Lease expiry can cause redelivery to another worker.
  • A worker can finish external work, lose its lease, and still report late.
  • A retry schedules a new durable attempt for the same logical activity execution.

Duplicate observation is therefore first-class behavior, not a bug condition. The application author must make the activity body or the remote system it calls safe to repeat.

See Activity Constraints for authoring guidance and Failures and Recovery for operator-facing recovery behavior.

What Is Exactly Once

Durable Workflow does not promise that a worker process sees side-effecting work only once. It does promise that committed durable facts are authoritative and do not duplicate for the same durable identifier.

In practice that means:

  • A committed workflow decision is persisted once in typed history for the durable command or step id it represents.
  • A committed terminal outcome for one activity attempt is persisted once for that activity_attempt_id.
  • Replay reads those committed facts back and rebuilds workflow state from them instead of re-running external work.

That split is the core mental model:

  • Transport and workers are at-least-once.
  • Committed durable history is exactly-once per durable identifier.

Lease Expiry and Redelivery

Lease expiry is a normal distributed-systems recovery path:

  • A claimed task carries a lease owner and expiry time.
  • If the lease expires before the worker reports progress or completion, the task becomes eligible for redelivery.
  • A different worker may then claim the same logical work.

Redelivery does not mean the engine forgot what already committed. It means the engine is recovering from uncertainty at the worker or transport layer.

When you see duplicate execution symptoms, ask two questions separately:

  1. Did the side effect happen more than once?
  2. Did the durable state record more than one committed outcome for the same durable identifier?

The first question is solved with idempotent activity design. The second is the engine contract.

Default Idempotency Surfaces

These identifiers are the stable places to dedupe work:

SurfaceWhat it identifiesTypical use
workflow_instance_idOne public workflow instanceDuplicate-start handling and business-level run identity
workflow_run_idOne specific durable runPinning one selected run for queries, export, or diagnostics
workflow_command_idOne mutating external commandClient-side request retry dedupe
activity_execution_idOne logical activity execution across retriesDefault remote idempotency key for external side effects
activity_attempt_idOne concrete attempt of that activityCorrelation when a remote system must distinguish separate tries
schedule_idOne schedule definitionDedupe around schedule ownership and trigger identity
message-stream idempotencyKeyOne retried logical message sendPrevent duplicate message ingestion when a sender retries

When in doubt, use activity_execution_id as the default idempotency key for an external operation. Reach for activity_attempt_id only when the external target truly needs every retry attempt to be distinguishable.

use Workflow\V2\Activity;

final class ChargeCard extends Activity
{
public function handle(array $payload): string
{
return app(PaymentGateway::class)->charge(
$payload,
idempotencyKey: $this->activityId(),
attemptCorrelation: $this->attemptId(),
);
}
}

What Developers Must Make Idempotent

You do not need to make workflow replay itself idempotent. The framework handles replay by rebuilding state from committed history.

You do need to make external effects safe to repeat, including:

  • payment or billing calls
  • emails, texts, and webhooks
  • writes to another database or service
  • file creation or upload
  • any command that creates or mutates state outside the workflow history

Common approaches:

  • Pass the remote API an idempotency key.
  • Write to a deterministic target resource such as a known object key.
  • Use an upsert or transaction keyed by a durable identifier.
  • Make the action naturally repeatable so the second call is a no-op.

Operator Guidance

When diagnosing a run in Waterline, the CLI, or server logs:

  • Treat duplicate activity observation after lease expiry as expected until the durable attempt outcome shows otherwise.
  • Treat late completion or failure reports as a race the engine resolves, not as proof that the remote side effect never happened.
  • Treat workflow-task replay as recovery, not as workflow-level retry.
  • Treat missing compatible workers, stuck leases, or repeated repair as operational signals that need investigation, not as a reason to assume the workflow body should re-run side effects.

The most important operator distinction is between transport uncertainty and durable outcome. Durable Workflow surfaces both so you can tell the difference.