Human-in-the-Loop
ctx.interrupt() suspends a run until someone answers a question: approve this,
pick one of these, edit these arguments. The run resumes with the recorded answer.
A signal says something happened; an interrupt asks.
from pyrula.workflows import workflow, InterruptResponse, InterruptAction
@workflow(name="expense_approval")async def expense_approval(ctx, amount_cents: int) -> str: response: InterruptResponse = await ctx.interrupt( "approve_expense", payload={"amount_cents": amount_cents}, expects="approval", )
if response.action == InterruptAction.APPROVE: await ctx.step( "disburse", lambda: disburse(amount_cents, idempotency_key=f"{ctx.run_id}:disburse"), ) return "approved" if response.action == InterruptAction.EDIT: await ctx.step( "disburse_edited", lambda: disburse( response.value["amount_cents"], idempotency_key=f"{ctx.run_id}:disburse_edited", ), ) return "approved with edit" return "rejected"The cycle
Section titled “The cycle”ctx.interrupt(event_id, payload) writes step:pending and run:interrupted, and the
run’s status becomes interrupted. A resume carrying an InterruptResponse emits
run:resumed, and interrupt returns that response. On replay the recorded response
comes straight back.
Each run:interrupted carries a wait_id. Pass it back with the answer: a resume
naming a wait that already resolved, or was superseded by a newer interrupt, is
refused.
Signature
Section titled “Signature”async def interrupt( self, event_id: str, payload: dict, *, expects: Literal["approval", "choice", "text", "value"] | None = None, options: list[str | dict] | None = None, # the choices, for expects="choice") -> InterruptResponse: ...expects tells a UI (and the cloud console) what kind of answer to collect.
InterruptResponse
Section titled “InterruptResponse”@dataclassclass InterruptResponse: action: InterruptAction | str value: Any = None| Action | Use for | value |
|---|---|---|
APPROVE | approval, proposed action | - |
REJECT | approval, proposed action | - |
RESPOND | choice, text, value | the answer |
EDIT | proposed action | the edited arguments |
Gate anything irreversible this way (charges, deletes, outbound email), and keep the approved effect idempotent. See Determinism.
To answer over HTTP, see the resume endpoint in Serving over HTTP.