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.

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(
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.

Raise one of these from a step to override retryable:

RaiseRetriedMeaning
Transientyes, up to max_attemptsA passing condition: network blip, lock contention.
PoisonneverBad input. Retrying can’t help.
FatalneverBroken invariant or missing config.
anything elseper 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.id

In 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.

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.

ExceptionRaised by
ChildWorkflowErrorctx.run_child when the child fails
QueryErrora query for an unknown name
RunNotFoundError / RunFinishedError / RunNotWaitingErrora client call on an unknown run / a finished run / an interrupt that isn’t open
UpdateRejected / UpdateError / UpdateTimeoutupdate when the validator rejects it / the handler raises / no worker applies it in time
DurableInvocationError / DurableInvocationTimeout / DurableInvocationCancelledrun_and_await and the integrations built on it
ContinuationChainErrorfollowing a continue_as_new chain that can’t be resolved

All are importable from pyrula.workflows.