Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

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"

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.

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.

@dataclass
class InterruptResponse:
action: InterruptAction | str
value: Any = None
ActionUse forvalue
APPROVEapproval, proposed action-
REJECTapproval, proposed action-
RESPONDchoice, text, valuethe answer
EDITproposed actionthe 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.