Errors and retries
A step that raises does not retry by default. The exception comes back out of
ctx.step, where the body can catch it. If nothing catches it, the run ends with
run:error.
from pyrula.workflows import RetryPolicy, workflow
@workflow(name="sync")async def sync(ctx, account: str) -> str: try: return await ctx.step( "pull", pull_account, account, retry=RetryPolicy(max_attempts=5, initial_backoff=0.5), timeout=30, ) except ConnectionError: return "skipped: upstream down"RetryPolicy
Section titled “RetryPolicy”RetryPolicy( max_attempts=3, # counts the first try; 1 = no retry initial_backoff=0.1, # seconds max_backoff=30.0, backoff_multiplier=2.0, retryable=None, # tuple of exception types; None = every Exception)retry= and timeout= (seconds per attempt) work on ctx.step and ctx.once. A
retried step records only its final outcome, so replay sees one result.
Saying what a failure means
Section titled “Saying what a failure means”Raise one of these from a step to override retryable:
| Raise | Retried | Meaning |
|---|---|---|
Transient | yes, up to max_attempts | A passing condition: network blip, lock contention. |
Poison | never | Bad input. Retrying can’t help. |
Fatal | never | Broken invariant or missing config. |
| anything else | per retryable |
from pyrula.workflows import Poison, Transient
def charge(order): resp = gateway.charge(order) if resp.status == 429: raise Transient("rate limited") if resp.status == 422: raise Poison(f"card rejected: {resp.reason}") return resp.idIn a workflow, Poison and Fatal behave the same: the step fails at once. Pick the one
that describes the failure, for whoever reads the error. A
Kafka binding then applies its own on_error disposition.
When the attempt itself breaks
Section titled “When the attempt itself breaks”Some failures happen before the run can record its own run:error: the workflow isn’t
registered on this worker, or a corrupt event breaks replay. The worker retries those up
to WorkerOptions.poison_max_retries (default 3) within poison_retry_ttl_s (1 hour),
then writes a terminal run:error so the run is never claimed again.
Runs no worker can serve because of a version mismatch
wait by default. Set stranded_policy=StrandedPolicy.FAIL (from pyrula.workflows.versioning)
and stranded_timeout on WorkerOptions to fail them with no_compatible_worker instead.
Caller errors
Section titled “Caller errors”| Exception | Raised by |
|---|---|
ChildWorkflowError | ctx.run_child when the child fails |
QueryError | a query for an unknown name |
RunNotFoundError / RunFinishedError / RunNotWaitingError | a client call on an unknown run / a finished run / an interrupt that isn’t open |
UpdateRejected / UpdateError / UpdateTimeout | update when the validator rejects it / the handler raises / no worker applies it in time |
DurableInvocationError / DurableInvocationTimeout / DurableInvocationCancelled | run_and_await and the integrations built on it |
ContinuationChainError | following a continue_as_new chain that can’t be resolved |
All are importable from pyrula.workflows.