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
|
|
|
# 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.
|
|
|
|
|
|
2026-08-20 11:20:23 +02:00
|
|
|
| Policy | `trigger.misfire_policy` value | Behaviour |
|
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
|
|
|
|---|---|---|
|
|
|
|
|
| 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. |
|
|
|
|
|
|
2026-08-20 11:20:23 +02:00
|
|
|
`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.
|
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
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-08-23 12:31:13 +02:00
|
|
|
do not produce duplicate run records. Task emission idempotency uses the
|
|
|
|
|
consumer reference recorded in `task_spawn_log`; runtime delivery uses the
|
|
|
|
|
unique `ops_runs.idempotency_key`.
|