# 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 | `dedupe_key_strategy` 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. | The `dedupe_key_strategy` on `ActivityDefinition` must match the `misfire_policy` on `CronTriggerConfig`. They are separate fields to allow the schedule configuration (how Temporal fires) to be decoupled from the workflow logic (how missed runs are handled). --- ## 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`.