Skip to main content

Durable Timer

Durable Timer is the Triggers platform component for workflow delays that must survive restarts and deployments. A flow sends an event into a Timer, the dedicated Timer service stores it, and Notify Triggers continues the released flow when the due time is reached.

It is a generic platform capability. It does not know what a prize, notification, payment, or campaign means.

Why it exists

A normal application timer ties the delay to one process. A deployment, crash, or ownership change can lose the wait or run it twice without a durable record. Moving the wait into every business service would also duplicate scheduling, recovery, and observability logic.

Durable Timer provides one execution contract for all SDK-enabled services:

CapabilityBehavior
Durable waitWaiting state is stored in NATS JetStream, not in process memory.
Event pass-throughThe exact event received on Start is saved and emitted on Time reached.
Restart recoveryNative schedules are reconciled after restart; overdue work becomes eligible immediately.
Flow-native retryA Condition may route to Retry and rearm the same Timer lineage.
Multiple TimersA released flow may use Timer nodes on different branches or in sequence.
Business isolationActions keep their ordinary request and result contracts. The SDK owns Timer control metadata.
Version safetyEvery occurrence resumes the released runtime version that created it, not the current Studio draft.

Use Durable Timer for workflow delays that require durable recovery. Keep short, action-local retries inside the action's normal retry policy.

What a flow author sees

A Timer has two inputs and one output:

  • Start creates a new durable occurrence from the current event.
  • Retry rearms that same occurrence after a downstream Condition chooses its retry branch.
  • Time reached continues the flow with the event saved at Start.

One-shot wait

Retry until a condition is met

The Check Prize action is not changed for this flow. It receives its normal request and returns its normal result. The SDK evaluates the Condition and, if the expression matches, publishes a Retry command that references the active Timer. If the expression does not match, the elapsed path completes. In v1 a Condition has one explicit output, so one Condition cannot both retry and send another normal branch onward. Explicit true/false branches are future work.

On the next due delivery, the flow again receives the event originally saved at Start. The Check Prize result controls the branch; it does not replace the saved event.

How data schemas move through a flow

Schemas follow the data edges of the released flow. They are not always the schema of the original source event.

NodeData schema available after the node
SourceThe source event schema.
Durable TimerThe schema of its Start input. Timer saves and later resumes the same event.
ServiceThe schema it received. A service handoff does not by itself transform the event.
ActionThe action's manifest result_event.fields. This is the action result, not the request sent to the action.
MappingThe fields declared by that mapping's projection.
ConditionNo new data schema. It reads the schema from its direct upstream node.

Retry is a control edge, not a data edge. It therefore does not participate in schema lineage. A Timer retry schedules the immutable event captured at its Start input; it does not schedule the result of the action that led to the Condition.

When a flow is released, the runtime writes the resolved direct-upstream fields into config.input_fields on every Condition, Mapping, and Action. The Go and Python SDKs use that immutable configuration for local validation and evaluation. Only releases made before this contract may fall back to absent input fields for compatibility.

Who owns each part

OwnerResponsibility
StudioTimer configuration, graph ports, authoring feedback, and layout.
Notify TriggersFlow authority, release validation, runtime compilation, initial routing, resume processing, and downstream continuation.
Triggers SDKPrivate Timer lineage, command construction, service-side Condition evaluation, and removal of control metadata before invoking actions.
Business actionDomain behavior and idempotent external side effects.
Durable Timer serviceOccurrence state, immutable start snapshot, retry count, wake scheduling, reconciliation, terminal archive, and resume publication.
NATS JetStreamDurable command, state, snapshot, schedule, wake, terminal, and resume storage.

The component that is currently executing the graph publishes the Timer command. Notify publishes it when the Timer or terminal feedback is reached in a coordinator-owned source/control path. An SDK publishes it when the same point is reached after a business-service handoff. Business services use their existing shared NATS connection, and Notify does not relay SDK-originated commands.

Release and execution model

The Studio graph is an authoring model. During release, Notify validates the Timer contract and compiles a deterministic internal resume boundary behind each Time reached edge.

Authors do not create or configure that internal boundary. An occurrence is identified by its released flow version, root execution, Timer node, stable edge lineage, and retry generation. This identity lets Notify and Durable Timer reject stale or unrelated control messages.

Authoring contract

Studio and release validation enforce the same rules:

RuleContract
Delay1,000 to 2,592,000,000 milliseconds, inclusive
Maximum retries0 to 100
Start inputExactly one incoming edge
Retry inputZero or one incoming edge
Time reached outputExactly one outgoing edge
Retry sourceA Condition reached from this Timer's elapsed path
Stable identityTimer control edges require unique, stable edge IDs
EnablementThe top-level node disabled field controls whether the Timer executes

Ordinary graph cycles remain invalid. The supported cycle is a downstream Condition returning to the Retry input of the same Timer.

Every elapsed branch must have one unambiguous terminal owner. A branch either hands work to a service or output, completes the Timer, retries it, or fails it. Release validation rejects ambiguous feedback paths.

Runtime lifecycle

No process sleeps for the configured delay. NATS 2.14 native scheduling drives the wake. Schedules operate at whole-second precision, so a delay is rounded up to the next whole-second boundary. An early wake is rejected.

Here, Owner is Notify while execution remains in a coordinator-owned path and the service SDK after a service handoff.

Delivery and recovery guarantees

  • The start snapshot and waiting state are durable before Start is acknowledged.
  • Startup and periodic reconciliation recreate the deterministic wake for each waiting occurrence.
  • An overdue occurrence is eligible immediately after recovery.
  • Compare-and-set updates, fences, deterministic IDs, and deduplication reject stale or duplicate control work.
  • Notify records resume progress in a MongoDB outbox before downstream publication.
  • Terminal state is archived before active state is removed.
  • Shutdown withdraws readiness and drains work; unfinished work is returned for redelivery.

The platform guarantee is at least once, not exactly once. NATS and MongoDB cannot commit atomically with each other or with a business system. Any action that can create an external side effect must therefore be idempotent.

Operational scope

Durable Timer is default-off and enabled centrally for the environment. Any valid released flow may contain multiple Timer nodes and may have multiple released versions. Business services do not carry a Timer flow list.

The supported behavior includes:

  • one-shot waits;
  • retry followed by completion;
  • retry-limit exhaustion;
  • sequential Timers;
  • restart with overdue recovery;
  • independent released flows using the same Durable Timer service.

Before production use, validate multi-replica availability, capacity, restore behavior, and the target environment's deployment configuration.

Coordination between different flows that operate on the same business entity is outside the current contract. Per-flow fairness and protection from noisy-neighbor load are backlog work.

Trust and data boundaries

V1 uses the platform's shared plain NATS 2.14 bus. It adds no Timer-specific TLS, credentials, ACL, certificate, or signing-key configuration. Access to the internal NATS network is the trust boundary.

Private Timer context is structurally validated and stripped before business handlers and public Kafka, HTTP, or output-topic boundaries. This protects business contracts from Timer internals, but it is not publisher authentication.

Further reading