Skip to content

Signals & Events

Signals let workflows pause and wait for external events—like user approvals, webhook callbacks, or manual triggers—before continuing execution. When a workflow waits for a signal, it releases the worker thread, making it available for other work.

Signals are durable: they are persisted to the backend and survive process restarts. Once a signal is received, the workflow can be resumed from where it left off.

Use wait_for_signal to pause a workflow until an external event arrives. The workflow will checkpoint its state and stop execution at that point.

from datetime import timedelta
from sayiir import task, Flow
@task
def create_order(order_id: int) -> dict:
return {"order_id": order_id, "status": "pending"}
@task
def fulfill(approval: dict) -> str:
return f"Fulfilled order (approved by {approval.get('by', 'unknown')})"
workflow = (
Flow("approval")
.then(create_order)
.wait_for_signal("manager_approval", timeout=timedelta(hours=24))
.then(fulfill)
.build()
)

The workflow will:

  1. Execute create_order
  2. Checkpoint and wait for the “manager_approval” signal
  3. Once received, execute fulfill with the signal payload

External systems send signals using send_signal (Python) or backend.send_event (Rust). You must provide:

  • The workflow instance ID
  • The signal name (must match the name in wait_for_signal)
  • A payload (any JSON-serializable data)
from sayiir import send_signal, InMemoryBackend
backend = InMemoryBackend()
# External system sends the signal
send_signal(
"order-42",
"manager_approval",
{"by": "alice", "approved_at": "2026-02-15T10:30:00Z"},
backend=backend
)

Signals are durably buffered in the backend. This means:

  • If a signal is sent before the workflow reaches wait_for_signal, it will be consumed immediately when the workflow gets there.
  • If a signal is sent after the workflow reaches wait_for_signal, the workflow will resume as soon as resume is called.
  • There are no race conditions—signals are never lost.

This buffering ensures that external systems can send signals at any time without worrying about timing.

from sayiir import run_durable_workflow, send_signal, resume_workflow
# Start the workflow
status = run_durable_workflow(workflow, "order-42", 42, backend=backend)
print(status) # WaitingForSignal
# Signal can be sent immediately or hours later
send_signal("order-42", "manager_approval", {"by": "alice"}, backend=backend)
# Resume to continue execution
status = resume_workflow(workflow, "order-42", backend=backend)
print(status) # Completed

You can specify an optional timeout when waiting for a signal. If the timeout expires before the signal is received, the workflow will fail with a timeout error.

from datetime import timedelta
# Wait up to 24 hours for approval
workflow = (
Flow("approval")
.then(create_order)
.wait_for_signal("manager_approval", timeout=timedelta(hours=24))
.then(fulfill)
.build()
)
# If no signal arrives within 24 hours, the workflow fails

Timeouts are also durable. If the process crashes during a timeout period, the remaining time will be calculated correctly when the workflow is resumed.

Here’s a full example showing the lifecycle of a workflow with signals:

from datetime import timedelta
from sayiir import (
task, Flow, run_durable_workflow, send_signal,
resume_workflow, InMemoryBackend
)
@task
def create_order(order_id: int) -> dict:
print(f"Creating order {order_id}")
return {"order_id": order_id, "status": "pending"}
@task
def fulfill(approval: dict) -> str:
approver = approval.get("by", "unknown")
print(f"Fulfilling order (approved by {approver})")
return f"Fulfilled order (approved by {approver})"
workflow = (
Flow("approval")
.then(create_order)
.wait_for_signal("manager_approval", timeout=timedelta(hours=24))
.then(fulfill)
.build()
)
backend = InMemoryBackend()
# Start workflow
status = run_durable_workflow(workflow, "order-42", 42, backend=backend)
print(f"Status: {status}") # WaitingForSignal
# External system sends approval
send_signal("order-42", "manager_approval", {"by": "alice"}, backend=backend)
# Resume workflow
status = resume_workflow(workflow, "order-42", backend=backend)
print(f"Final status: {status}") # Completed