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