Coordinate timers, signals, and durable waits
Suspend workflows without holding a process, deliver idempotent signals, govern long waits, and resume safely.
Why waits are durable state
Long business processes spend most of their lifetime waiting: a delivery date, approval, webhook, human answer, or external Batch. Keeping a Worker or async task alive wastes capacity and loses state on restart.
A durable wait commits why execution paused and what event may wake it. The Worker releases ownership; another process can resume later.
Timers
A Timer records a store-authoritative wake time. Claim queries do not return it before that instant. When due, it becomes ready without relying on a process sleep or in-memory scheduler.
Use Timers for retry schedules, reminders, polling intervals, and business deadlines. Keep operation timeouts separate: a Timer decides when to resume, while a timeout bounds one attempt.
Signals
A Signal is external input addressed to a waiting Workflow. Give every delivery a stable identity so network retries cannot create duplicate transitions. Store Signals durably and scope them to tenant and checkpoint.
Define behavior for early, duplicate, unknown, and late Signals. Never let an opaque Task or Signal ID act as authorization by itself.
Interrupts
An interrupt moves a Workflow to explicit input_required state. It identifies
the requested decision and accepted response shape. Approve, edit, reject, or
cancel commands must be idempotent.
Human review is not a special prompt. It is a durable protocol boundary with identity, authorization, retention, and auditable resolution.
Retention and governance
Bound wait duration, pending Signal count, polling frequency, and retained history. Supervisors should find stale waits and surface them without inventing completion.
Protocol TTL, execution deadline, business due date, and physical deletion retention are distinct concepts. Model each separately; converting one into another can cancel live work or preserve sensitive state indefinitely.
Build timer, signal, and review waits
use std::time::Duration;
let workflow = Workflow::builder("approval")
.version(1)
.step("prepare", PrepareProposal, CapabilitySet::new())
.wait_for_signal_or_timeout(
"wait_for_documents",
"documents_ready",
Duration::from_secs(24 * 60 * 60),
)
.interrupt("manager_review", "Approve, edit, or reject this proposal")
.timer("cooldown", Duration::from_secs(30))
.step("commit", CommitDecision, write_capabilities)
.build()?;The worker checkpoints the wait before releasing its lease. Publish Signals
through WorkflowStore with a stable WorkflowSignalId, authenticated tenant,
checkpoint ID, name, and payload. Treat duplicate delivery as the same event.
For interrupts, render the keyed input request and submit one idempotent
approve/edit/reject decision; never infer approval from a missing response.