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:
- The Event Router computes the workflow ID as
activity-{activity_id}:{event.event_id}. - It calls
client.start_workflow(..., id=workflow_id, id_reuse_policy=REJECT_DUPLICATE). - 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.