activity-core/docs/idempotency.md
tegwick 944fd158de
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 18s
Repair production automation truth and schedule cleanup
2026-08-20 11:20:23 +02:00

3.2 KiB

Idempotency Contract

Workflow ID strategy

Every RunActivityWorkflow execution has a deterministic, stable workflow ID. This is the primary idempotency mechanism — Temporal rejects duplicate starts with the same workflow ID in the same namespace.

Trigger type Workflow ID format
Cron / scheduled activity-{activity_id}:{scheduled_for.isoformat()}
External event activity-{activity_id}:{event.event_id}

Example (cron): activity-550e8400-e29b-41d4-a716-446655440000:2026-03-01T09:00:00+00:00 Example (event): activity-550e8400-e29b-41d4-a716-446655440000:evt_abc123

Both formats guarantee that re-delivering a trigger (broker retry, duplicate schedule fire) produces at most one workflow execution.


Misfire policy

A misfire occurs when a scheduled trigger fires after its nominal time because the worker was down, overloaded, or the schedule was paused.

Policy trigger.misfire_policy value Behaviour
Skip "skip" Missed runs are discarded. Temporal ScheduleOverlapPolicy.SKIP.
Catch up "catchup" Missed runs are replayed up to 10 times (configurable). Uses schedule.handle.backfill().
Compress "compress" One run is executed with a widened context window covering all missed intervals.

trigger.misfire_policy is the runtime control. The persisted dedupe_key_strategy field is legacy API/DB compatibility metadata; schedule execution does not read it. In particular, dedupe_key_strategy: skip does not suppress a later scheduled fire whose resolved context or emitted task content matches an earlier fire. New definitions should configure only the trigger policy and should not rely on dedupe_key_strategy for content deduplication.

Each nominal cron time produces a different workflow ID. If an hourly rule sees the same unread message on consecutive hours, both hours may emit work. That requires an explicit domain idempotency key or consumer-side state transition; it is not a Temporal misfire concern.


Event deduplication

For event-driven activities:

  1. The Event Router computes the workflow ID as activity-{activity_id}:{event.event_id}.
  2. It calls client.start_workflow(..., id=workflow_id, id_reuse_policy=REJECT_DUPLICATE).
  3. If the workflow ID already exists (event was already processed), Temporal returns WorkflowAlreadyStartedError — the router logs it and moves on.

Prerequisite: every inbound event must have a stable event_id. Events without a stable ID must be assigned one by the ingress boundary before entering the system.


Task instance idempotency

Each TaskInstance spawned by RunActivityWorkflow gets its own unique workflow ID:

task-{run_id}:{task_type}:{index}

This ensures that if RunActivityWorkflow is replayed by Temporal (e.g. after a worker restart), it does not re-spawn task instances that were already started.


Database idempotency

activity_runs uses run_id as the primary key (UUID). The log_run activity uses an upsert (INSERT ... ON CONFLICT DO NOTHING) so that Temporal activity retries do not produce duplicate run records.

task_instances similarly uses an upsert on id.