Files
activity-core/docs/idempotency.md
tegwick 6f9132314f Add project scaffold: contracts, schemas, docker-compose, workplans
Phase 0 contracts (event envelope, ActivityDefinition, idempotency doc,
naming conventions) and Phase 1 Temporal cluster setup (docker-compose.dev.yml,
Temporal dynamic config) are complete. Includes Pydantic models, JSON schemas,
wiki architecture docs, and ADR-001 workplan files for both workstreams.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 22:45:40 +01:00

2.7 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 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.