Skip to content

Tutorial: Approval Workflow (Signals)

Build a human-in-the-loop expense approval workflow using signals. The workflow submits an expense, parks until a manager approves or the timeout expires, then processes the result.

  1. Workflow reaches wait_for_signal() and parks — no workers held
  2. External system sends a signal (API call, webhook, CLI)
  3. Workflow resumes with the signal payload as input to the next step
  4. Optional timeout triggers escalation if no response
from datetime import timedelta
from sayiir import task, Flow, run_durable_workflow, send_signal, resume_workflow, InMemoryBackend
@task
def submit_expense(expense: dict) -> dict:
return {**expense, "status": "pending_approval"}
@task
def process_approved(approval: dict) -> str:
return f"Expense approved by {approval['approver']} — processing reimbursement"
workflow = (
Flow("expense-approval")
.then(submit_expense)
.wait_for_signal("manager_approval", timeout=timedelta(hours=48))
.then(process_approved)
.build()
)
backend = InMemoryBackend()
# Submit — parks at signal
status = run_durable_workflow(workflow, "exp-001", {"employee": "Alice", "amount": 250.00}, backend=backend)
# Manager approves (could be minutes or days later, from any process)
send_signal("exp-001", "manager_approval", {"approver": "Bob", "decision": "approved"}, backend=backend)
# Resume — processes approval
status = resume_workflow(workflow, "exp-001", backend=backend)
print(status.output)

In production, the submit and approve steps happen in separate processes — the workflow durably parks in PostgreSQL until the signal arrives.