fusee

Fusee for Python: durable execution at the speed of the disk.

Definitions are plain values, apart from any engine::

import fusee

review = fusee.signal("review", bool)

@fusee.workflow("checkout")
async def checkout(ctx: fusee.Ctx, order: Order) -> str:
    quote = await ctx.step("quote", lambda: pricing.quote(order))
    charge = await ctx.step("charge", lambda s: payments.charge(quote, key=s.key),
                            retry=fusee.Retry(attempts=5))
    approved = await ctx.wait_for(review, timeout="1d")
    return charge["id"] if approved else await ctx.step("refund", lambda: payments.refund(charge))

engine = await fusee.Engine.open("./data", workflows=[checkout])
run = await engine.start(checkout, "order-123", order)
await run.signal(review, True)
print(await run.result())

Workflow code runs under Fusee's deterministic scheduler, so it may only await ctx operations (or ctx.all / ctx.race of them and of other workflow coroutines). Step functions run on the asyncio loop (or a thread, with thread=True) and may do anything.

@dataclasses.dataclass(frozen=True)
class Concurrency:

Runs at once: at most limit runs of the type run at a time, or of each key (key(input), a tenant say); the others wait, in the order they were started, as fusee.queued/<id> runs, and result() waits through the queue. The turns are durable: a run keeps its turn while it sleeps and across crashes. Starts from outside a workflow take turns; a child started by a workflow does not.

Concurrency(limit: int, key: Callable[[Any], str] | None = None)
limit: int
key: Callable[[Any], str] | None = None
@contextlib.contextmanager
def as_requester(by: str):

Names who asks for the signals, cancellations, terminations and resets made inside the block: their journal records carry it as by (audit). The HTTP API names its token or the proxy's signed-in user the same way.

def workflow( name: Any = None, fn: Any = None, /, *, version: int = 1, timeout: Any = None, input: Any = None, output: Any = None, concurrency: Concurrency | None = None) -> Any:

Defines a workflow type::

@fusee.workflow("checkout")
async def checkout(ctx: fusee.Ctx, order: Order) -> str: ...

@fusee.workflow alone names it after the function. The input's and result's annotations (a dataclass, pydantic model, msgspec struct, TypedDict, datetime, ...) decide what the code receives and what run.result() returns; input= and output= override them. timeout is the workflow's deadline from its start.

def signal(name: str, type: Type[~T] | None = None) -> Signal[~T]:

Defines a signal. A plain string works wherever a signal does.

def query( name: str, *, arg: Any = None, result: Any = None) -> Query[typing.Any, typing.Any]:

Defines a query: status = fusee.query("status", result=Status).

def update( name: str, *, arg: Any = None, result: Any = None) -> Update[typing.Any, typing.Any]:

Defines an update: approve = fusee.update("approve", arg=Approval, result=bool).

def object( name: str, handlers: dict | None = None, *, version: int = 1) -> ObjectType:

Defines a keyed object type (see ObjectType).

def shared(fn: Callable[..., Any]) -> Any:

Marks an object handler as shared: it runs as soon as its call arrives, next to other calls, and only reads state.

class Workflow(typing.Generic[~I, ~O]):

A workflow type. Calling it runs the function directly (for tests).

Workflow( name: str, fn: Callable[..., Awaitable[~O]], *, version: int = 1, timeout=None, input: Any = None, output: Any = None, concurrency: Concurrency | None = None)
name
version
concurrency
fn
timeout
def decode_input(self, v: Any) -> Any:
def decode_output(self, v: Any) -> Any:
def check_input(self, v: Any) -> None:

Validates an input from outside before it is recorded (a refusal records nothing). Only for annotated types that validate.

class Signal(typing.Generic[~T]):

A signal's name and payload type: review = fusee.signal("review", bool).

Signal(name: str, type: Any = None)
name
type
def decode(self, v: Any) -> Any:
class Query(typing.Generic[~A, ~R]):

Abstract base class for generic types.

On Python 3.12 and newer, generic classes implicitly inherit from Generic when they declare a parameter list after the class's name::

class Mapping[KT, VT]:
    def __getitem__(self, key: KT) -> VT:
        ...
    # Etc.

On older versions of Python, however, generic classes have to explicitly inherit from Generic.

After a class has been declared to be generic, it can then be used as follows::

def lookup_name[KT, VT](mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
    try:
        return mapping[key]
    except KeyError:
        return default
Query(name: str, *, arg: Any = None, result: Any = None)
name
arg
result
class Update(typing.Generic[~A, ~R]):

Abstract base class for generic types.

On Python 3.12 and newer, generic classes implicitly inherit from Generic when they declare a parameter list after the class's name::

class Mapping[KT, VT]:
    def __getitem__(self, key: KT) -> VT:
        ...
    # Etc.

On older versions of Python, however, generic classes have to explicitly inherit from Generic.

After a class has been declared to be generic, it can then be used as follows::

def lookup_name[KT, VT](mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
    try:
        return mapping[key]
    except KeyError:
        return default
Update(name: str, *, arg: Any = None, result: Any = None)
name
arg
result
class ObjectType:

A keyed object type. Handlers are exclusive (one call at a time per key, in arrival order; they may change state) unless marked shared::

cart = fusee.object("cart", {"add": add, "items": fusee.shared(items)})

or with decorators::

cart = fusee.object("cart")

@cart.exclusive
async def add(ctx, item): ...
ObjectType(name: str, handlers: dict | None = None, *, version: int = 1)
name
version
handlers: dict
def exclusive(self, fn=None, *, name: str | None = None):

Adds an exclusive handler (async def h(ctx, arg)).

def shared(self, fn=None, *, name: str | None = None):

Adds a shared handler: it runs at once, next to other calls, and only reads state.

@dataclasses.dataclass
class Retry:

How a failing step is retried. attempts includes the first.

A failure marked final (fusee.Fatal, or any exception with retryable = False) is never retried. non_retryable names exception types that are final; retry_if(failure, attempt) decides each time. Each delay is drawn between half of it and all of it unless jitter=False; no retry starts later than max_elapsed after the first failure. on_exhausted="pause": when the retries run out on a failure that could be retried, the run pauses (its parent and callers keep waiting) until run.resume() runs the step again with a fresh set of attempts; "fail" fails the step; None takes the engine's on_exhausted.

Retry( attempts: int = 3, initial: int | float | str = 100, factor: float = 2.0, max: int | float | str = '10s', jitter: bool = True, max_elapsed: None | int | float | str = None, non_retryable: tuple = (), retry_if: Callable[[Failure, int], bool] | None = None, on_exhausted: str | None = None)
attempts: int = 3
initial: int | float | str = 100
factor: float = 2.0
max: int | float | str = '10s'
jitter: bool = True
max_elapsed: None | int | float | str = None
non_retryable: tuple = ()
retry_if: Callable[[Failure, int], bool] | None = None
on_exhausted: str | None = None
def delay_ms(self, failed_attempt: int) -> float:
def allows(self, f: Failure, attempt: int) -> bool:
def retryable(self, f: Failure, attempt: int) -> bool:

Whether the policy retries f at all (it is not final), whatever attempts remain.

@dataclasses.dataclass(frozen=True)
class IdPolicy:

What a start does when its workflow ID is taken. running: fail, use-existing, terminate-existing. finished: allow, allow-if-failed, refuse, use-existing.

IdPolicy(running: str = 'fail', finished: str = 'allow')
running: str = 'fail'
finished: str = 'allow'
def bits(self) -> int:
@dataclasses.dataclass
class ScheduleSpec:

When a schedule fires: a cron expression or an interval.

ScheduleSpec( cron: str | None = None, every: Any = None, offset: Any = None, timezone: str = 'UTC', overlap: str = 'skip', jitter: Any = None, catchup_window: Any = None, paused: bool = False)
cron: str | None = None
every: Any = None
offset: Any = None
timezone: str = 'UTC'
overlap: str = 'skip'
jitter: Any = None
catchup_window: Any = None
paused: bool = False
@classmethod
def of(cls, spec: Any) -> ScheduleSpec:
def wire(self, workflow: str, input: Any) -> dict:
class Engine(fusee._runtime.CodeHost):

An embedded engine: it records runs in a directory (or memory) and runs the workflows it was opened with.

Engine(loop: asyncio.events.AbstractEventLoop, **opts)
@classmethod
async def open( cls, dir: str | None = None, *, workflows: Iterable[Any] = (), objects: Iterable[ObjectType] = (), durability: str | None = None, address: str | None = None, profile: str | None = None, replica_id: int | None = None, replica_metrics: ReplicaMetrics | None = None, peers: list[str] | None = None, shards: int | None = None, retention=None, park_after=None, checkpoint_interval=None, flush: str | None = None, segment_bytes: int | None = None, keep_history: bool | None = None, archive_history: bool = False, record_stacks: bool = True, on_exhausted: str = 'fail', sync_steps: str = 'inline', codec: Engine.list | None = None, telemetry: Any = None, inline_flush: bool | None = None, http: str | None = None) -> Engine:

Opens an engine that runs workflows (and objects).

With no directory: FUSEE_DIR; else FUSEE_URL (a worker of a Fusee server); else ./fusee-data. durability is local (the default with a directory), memory, server or replicated.

profile="dev" (or FUSEE_PROFILE=dev) is for development: it says so on stderr, keeps every run's history, flushes with fsync rather than F_FULLFSYNC on macOS (it survives a process crash, not power loss), warns when a synchronous step holds the event loop, replays each run against the code when it parks or ends and logs any difference, makes result() of a stuck run raise DeterminismError, shows ctx.log records on stderr when logging is not configured, and serves the UI.

http="127.0.0.1:7601" serves the HTTP API and UI from the engine ("off": never; under the dev profile the default is FUSEE_HTTP, else 7601, else a free port).

record_stacks=False keeps stack traces out of recorded failures. on_exhausted="pause" pauses a run whose step runs out of retries (unless the step's Retry says otherwise) until run.resume(). sync_steps="thread" runs synchronous step functions on threads. codec=aes_gcm({...}) encrypts payloads; telemetry= open_telemetry() reports spans and metrics.

def serve( self, addr: str = '127.0.0.1:7601', *, token: str | None = None, insecure: bool = False) -> str:

Serves the HTTP API and the web UI on a thread of their own (they call this engine through its event loop; nothing rides the step path). Off loopback a token is required. Returns the URL::

print(engine.serve())   # http://127.0.0.1:7601
def register(self, *definitions: Any) -> None:

Adds workflows or object types after the engine opened (prefer Engine.open(workflows=[...])).

def on(self, event: str, handler: Callable[..., Any]) -> None:

Calls handler on an engine event: "stuck" (workflow_id, message) when a run stops on a determinism error, "paused" (workflow_id, step, failure) when a step of this process runs out of retries and pauses its run, and "waiting_for_code" (workflow_id, type) when a run has waited five seconds for code no process registered.

def run( self, id: str, workflow: Workflow[Any, ~O] | None = None) -> Run[~O]:

A handle to a run by workflow ID; workflow types its result.

async def start( self, workflow: Any, id: str, input: Any = None, *, id_policy: Any = None, timeout: Any = None, version: int | None = None, delay: Any = None, at: Any = None) -> Any:

Starts a run of workflow (a definition or a type name) under workflow ID id; returns once it is durable. id_policy (an IdPolicy or {"running": ..., "finished": ...}) says what happens when the ID is taken. delay="10m" or at=datetime starts it later: a durable timer (the run fusee.delay/<id>), then the start; result() waits through the delay.

async def signal_with_start( self, workflow: Any, id: str, input: Any, signal: Any, value: Any = None, *, id_policy: Any = None, timeout: Any = None, version: int | None = None) -> Any:

Signals the run id, starting it first with input if it is not running: the signal is recorded before the new run's code first runs. Returns the run::

await engine.signal_with_start(cart, "cart-7", Cart(), add, item)
async def update_with_start( self, workflow: Any, id: str, input: Any, update: Any, arg: Any = None, *, update_id: str = '', id_policy: Any = None, timeout: Any = None, version: int | None = None) -> tuple:

Starts the workflow if it is not running and delivers an update in the same record. Returns (run, result).

def object(self, type: Any, key: str) -> ObjectRef:

Key key of object type type (a definition or a name).

schedules: Schedules
async def schedule( self, name: str, spec: Any, workflow: Any, input: Any = None) -> Schedule:

Creates a schedule (or updates its spec) that starts workflow with input on spec: a cron string, a dict ({"every": "1h"}) or a ScheduleSpec.

async def list( self, *, type: Any = None, status: str | None = None, started_after: Any = None, started_before: Any = None, attributes: dict | None = None, limit: int = 1000) -> AsyncIterator[RunSummary]:

Runs, newest first per shard, filtered by type, status, start time and search attributes ({"amount": {"gte": 100}})::

async for r in engine.list(status="failed"):
    print(r.id, r.error)
async def complete(self, token: str, value: Any = None) -> None:

Finishes an async completion with a value; returns once recorded durably.

async def fail(self, token: str, failure: Any) -> None:

Fails an async completion (a message, an exception or a Failure).

async def debounce( self, workflow: Any, key: str, input: Any = None, *, window: Any, max_wait: Any = None, dedup_window: Any = None, event_id: str = '') -> Run[typing.Any]:

Debounces a start under key: starts inside the window collapse into one run, which begins with the latest start's input when the window closes (window after the latest start, or max_wait after the first). Returns once this start is durable, with the handle of that run (await run.started()). event_id makes a retried call join once.

async def batch( self, workflow: Any, key: str, event: Any = None, *, max_events: int | None = None, max_wait: Any = None, dedup_window: Any = None, event_id: str = '') -> Run[typing.Any]:

Adds an event to the batch under key: once it holds max_events events (default 100) or max_wait (default 1s) has passed since its first, one run starts with all of them as its input (a list).

def limit(self, name: str, *, concurrent: int = 0, per_second: float = 0.0) -> None:

Defines (or resizes) a concurrency and rate limit for steps.

def limits(self) -> list[LimitInfo]:

Every limit this engine defined, with its permits now, by name (fusee limits, the UI's Limits page).

def limit_usage(self, name: str) -> tuple:

(in use, waiting here) permits of a limit right now.

async def metrics(self) -> dict:

Counters summed over shards (as GET /v1/metrics's total), with the runs stuck and waiting for code.

def shard_metrics(self) -> list[dict]:

Per-shard counters in shard-number order, durations in microseconds.

def export_journal(self, id: str) -> bytes | None:

A run's journal as an export file (.fjnl) for Replayer.

async def close(self) -> None:
class Client:

Starts, signals and reads runs through a Fusee server. The same interface as Engine::

client = await fusee.Client.connect("db1:7600", token_file="/etc/fusee/token")
run = await client.start(checkout, "order-123", order)
Client(transport, address: str = '')
@classmethod
async def connect( cls, address: str | None = None, *, tls: dict | None = None, token_file: str | None = None) -> Client:

Connects to a server: address (default FUSEE_URL) and, as options, tls={"ca": ..., "cert": ..., "key": ...} and token_file.

async def call(self, method: str, args: dict) -> Any:

Sends one protocol call without blocking the event loop.

def run( self, id: str, workflow: Workflow[Any, ~O] | None = None) -> Run[~O]:
async def start( self, workflow: Any, id: str, input: Any = None, *, id_policy: Any = None, timeout: Any = None, version: int | None = None, delay: Any = None, at: Any = None) -> Any:

See Engine.start (delay=/at= start it later).

async def signal_with_start( self, workflow: Any, id: str, input: Any, signal: Any, value: Any = None, *, id_policy: Any = None, timeout: Any = None, version: int | None = None) -> Any:
async def update_with_start( self, workflow: Any, id: str, input: Any, update: Any, arg: Any = None, *, update_id: str = '', id_policy: Any = None, timeout: Any = None, version: int | None = None) -> tuple:
def object(self, type: Any, key: str) -> ObjectRef:
schedules: Schedules
async def schedule( self, name: str, spec: Any, workflow: Any, input: Any = None) -> Schedule:
async def list( self, *, type: Any = None, status: str | None = None, started_after: Any = None, started_before: Any = None, attributes: dict | None = None, limit: int = 1000) -> AsyncIterator[RunSummary]:
async def complete(self, token: str, value: Any = None) -> None:
async def fail(self, token: str, failure: Any) -> None:
async def debounce( self, workflow: Any, key: str, input: Any = None, *, window: Any, max_wait: Any = None, dedup_window: Any = None, event_id: str = '') -> Run[typing.Any]:
async def batch( self, workflow: Any, key: str, event: Any = None, *, max_events: int | None = None, max_wait: Any = None, dedup_window: Any = None, event_id: str = '') -> Run[typing.Any]:
async def close(self) -> None:
def connection_url( address: str, *, tls: dict | None = None, token_file: str | None = None, max_shards: int | None = None, shared_memory: bool = False) -> str:

A server address with its options in the form the native layer reads.

class Run(typing.Generic[~O]):

A handle to a run by workflow ID, from an engine or a client.

Run(ops: Any, id: str, workflow: Workflow | None = None)
id
async def signal(self, signal: Any, value: Any = None) -> None:

Sends a signal: await run.signal(review, True).

async def query(self, query: Any, arg: Any = None) -> Any:

Calls a query handler; the answer waits until the state it read is durable.

async def update(self, update: Any, arg: Any = None, *, id: str = '') -> Any:

Sends an update and waits for its handler's durable result. A retried call with the same id gets the recorded result.

async def result(self) -> ~O:

The run's result (following continue-as-new and reset). A failed run raises its error: a step's as StepError (e.cause is the step's failure), anything else as WorkflowError.

async def status(self) -> RunSummary:

The run's summary. Raises NotFoundError if there is none.

async def describe(self) -> RunDetail:

The run's summary and journal.

async def started(self) -> None:

Waits until a run with this ID exists (a debounced or batched run starts only when its window closes).

async def cancel(self, reason: str = 'cancelled') -> None:

Delivers cancellation to the run's waits; its code may compensate.

async def terminate(self, reason: str = 'terminated') -> None:

Ends the run at once; children follow their parent close policy.

async def resume(self) -> None:

Resumes a run that paused because a step ran out of retries (Retry(on_exhausted="pause")): the step runs again at once, with a fresh set of attempts, on the code this process has now. Raises RejectedError if nothing is paused.

async def reset( self, before_step: str, *, reapply_signals: bool = True, version: int = 0) -> None:

Starts a new run whose journal is this one's cut before before_step.

async def retry(self) -> None:

Runs a failed run again from its failed step: a reset before it (the steps before keep their results).

def stream( self, name: str, *, from_: int = 0, wait=None) -> StreamReader:

A reader of the run's stream name from offset from_.

@dataclasses.dataclass
class RunSummary:

One run, as listings and run.status() give it.

RunSummary( id: str, type: str, version: int, status: str, started_ms: int, finished_ms: int | None = None, error: str | None = None, run_id: int = 0, shard: int = 0, attributes: dict = <factory>, object: bool = False)
id: str
type: str
version: int
status: str
started_ms: int
finished_ms: int | None = None
error: str | None = None
run_id: int = 0
shard: int = 0
attributes: dict
object: bool = False
finished: bool
@classmethod
def from_json(cls, r: dict) -> RunSummary:
@dataclasses.dataclass
class RunDetail:

A run and its journal: each record a dict with its kind (start, step, attempt, timer, signal, wait, child, child_done, send, update, update_done, completed, heartbeat, attributes, chunk, stream_end, done, ...), times and run IDs left out.

RunDetail( summary: RunSummary, journal: list, object: dict | None = None)
summary: RunSummary
journal: list
object: dict | None = None
steps: list

The names of the steps the run recorded, in order.

failed_step: str | None

The step whose failure ended the run, if one did.

@dataclasses.dataclass
class LimitInfo:

A limit and its permits right now (engine.limits()).

LimitInfo( name: str, concurrent: int, per_second: float, in_use: int, waiting: int)
name: str
concurrent: int
per_second: float
in_use: int
waiting: int
class ObjectRef:

A key of an object type, called from outside workflow code.

ObjectRef(ops: Any, type: str, key: str)
type
key
id: str
async def call(self, handler: str, arg: Any = None, *, id: str = '') -> Any:

Calls a handler and waits for its durable result. Calls with the same id run once, and each gets the recorded result.

async def send(self, handler: str, arg: Any = None, *, id: str = '') -> None:

Calls a handler without waiting for its result: returns once the call is accepted.

async def edit_state(self, edit: dict, *, if_version: str | None = None, id: str = '') -> dict:

Edits the state from outside its handlers: an exclusive call, so it waits for the key and commits as a handler's writes do. edit has clear_all/clearAll, clear (fields), set (fields and values; None removes one) and patch (a JSON Patch over the state as one object), applied in that order. if_version refuses the edit unless the state is still at that version (the version in describe()'s object). Returns {"version", "changed"}.

async def set_state(self, field: str, value: Any, *, if_version: str | None = None) -> dict:

Sets one field of the state.

async def patch_state(self, ops: list, *, if_version: str | None = None) -> dict:

Applies a JSON Patch (RFC 6902) to the state as one JSON object.

async def clear_state(self, field: str | None = None, *, if_version: str | None = None) -> dict:

Clears one field, or (none named) the whole state; the key remains.

async def state(self) -> dict:

The committed state ({} for a key never called), once durable.

async def describe(self) -> RunDetail | None:

The key's run: its journal, and object with its state and pending calls.

async def delete(self) -> None:

Deletes the key: its run ends and its state with it.

class Schedule:

A schedule, addressed by name.

Schedule(ops: Any, name: str)
name
id
async def pause(self) -> None:
async def resume(self) -> None:
async def trigger(self) -> None:

Starts one run now.

async def backfill(self, from_: Any, to: Any) -> None:

Starts a run for every scheduled time in [from_, to] (datetimes or Unix milliseconds).

async def describe(self) -> dict:
async def delete(self) -> None:

Stops the schedule; runs it started keep running.

class Schedules:

engine.schedules.get(name), engine.schedules.list().

Schedules(ops: Any)
def get(self, name: str) -> Schedule:
async def list(self) -> list:
class StreamReader:

A reader of a run's stream: async for chunk in run.stream("tokens") yields each chunk once it is durable, in order, and ends once the stream is complete. offset is where a reader that reconnects continues; status says how the stream ended (ok, a failed step's failed, or the run's status if it ended first).

StreamReader( read: Callable[[int | None, int, int], Any], start: int = 0, wait=None, run: int | None = None)
offset
run
status: str | None
class ReplicaMetrics:

Observe a replica, including while its worker waits to become primary.

def snapshot(self):
class Ctx:

The workflow context. Code between ctx calls must be deterministic: it runs again against the journal after a crash.

Ctx(x: fusee._runtime.RunExec)
def compensate(self, name: str, fn: Callable[..., Any]) -> None:

Registers a step that undoes what the workflow did so far: if the workflow fails or is cancelled, every registered compensation runs as a step named name, newest first, then the failure is raised. Register it after the step it undoes succeeded::

hold = await ctx.step("reserve", lambda: inventory.reserve(sku))
ctx.compensate("release", lambda: inventory.release(hold))
await ctx.step("charge", lambda: payments.charge(cents))  # fails: "release" runs
workflow_id: str
workflow_type: str
version: int
run_id: int
log: logging.LoggerAdapter

A logger tagged with the workflow ID and run (the fusee logger). Silent while the run replays its journal, so a line is written once however often the code runs; not journaled.

def step( self, name: str, fn: Callable[..., Any], *, retry: Retry | None = None, timeout=None, heartbeat_timeout=None, limit: str | None = None, read_only: bool = False, priority: int | None = None, fairness_key: str | None = None, fairness_weight: int | None = None, output: Any = None, thread: bool | None = None) -> Any:

Runs fn (sync or async) as a durable step and returns its recorded result (as JSON decodes it, or as output's type).

fn may take one argument, the step context s: s.key (an idempotency key), s.attempt, s.heartbeat(p), s.stream(n). It runs once everything the run recorded before is durable; with read_only=True at once (use it only for steps that change nothing outside). thread=True runs a synchronous fn on a thread so it does not hold the event loop.

With limit, each attempt first takes a permit of that limit (engine.limit); a higher priority goes first, and steps of one priority take turns by fairness_key.

def await_completion( self, name: str, send: Callable[[Completion], Any], *, retry: Retry | None = None, timeout=None, output: Any = None) -> Any:

A step completed from outside: send(cb) starts work elsewhere with cb.token (or cb.url); the workflow waits until a process calls engine.complete(token, value) or engine.fail(token, error). send is retried under the retry policy, each attempt with a new token.

def stream(self, name: str) -> StreamWriter:

Opens the run's stream name for appending (readers follow it with run.stream(name)). Its appends are recorded once (a replay appends nothing), and close() or the run's end completes it. A step opens a stream of its own with s.stream(name).

def sleep(self, name: str, duration) -> Any:

Sleeps durably: the wake time is recorded and survives restarts.

def sleep_until(self, name: str, at) -> Any:

Sleeps until at (a datetime or Unix milliseconds).

def wait_for(self, signal: Any, *, timeout=None) -> Any:

Waits for the next signal; None on timeout.

def child( self, workflow: Any, id: str, input: Any = None, *, parent_close: str = 'terminate', timeout=None) -> Any:

Starts a child workflow and waits for its result. The child's ID makes the start idempotent.

def start_child( self, workflow: Any, id: str, input: Any = None, *, parent_close: str = 'terminate', timeout=None) -> None:

Starts a child without waiting. By default it is terminated when this workflow closes; parent_close="abandon" lets it outlive it.

def signal_workflow(self, id: str, signal: Any, value: Any = None) -> None:

Signals another workflow, once this point is durable.

def cancel_workflow(self, id: str, *, reason: str = 'cancelled') -> None:

Asks another workflow to cancel, once this point is durable.

def object(self, type: Any, key: str) -> ObjectCall:

Key key of object type type, called from this workflow: each call is a journal operation, run exactly once.

def on_query(self, query: Any, handler: Callable[[Any], Any]) -> None:

Registers a read-only query handler (run.query(query, arg)).

def on_update( self, update: Any, handler: Callable[[Any], Any], *, validate: Callable | None = None) -> None:

Registers an update. validate runs first (raise, or return a string, to refuse; a refusal records nothing); handler (sync or async, may run steps) runs once per accepted update.

def continue_as_new(self, input: Any = None, *, version: int = 0) -> Any:

Ends this run and starts a new run of the same ID with input, in one record. Await it; it never returns.

def journal_size(self) -> int:

Journal operations this run made so far (deterministic).

def patched(self, id: str) -> bool:

True for new runs, and for running ones the first time they get here after the deploy; false while replaying an older journal.

def deprecate_patch(self, id: str) -> None:
def set_attributes(self, attributes: dict) -> None:

Records search attributes (None removes a key). Never encrypted.

def now(self) -> int:

Unix milliseconds, recorded.

def random(self) -> float:

A float in [0, 1), recorded.

def uuid(self) -> str:
def all(self, items: list) -> Any:

Runs ops and coroutines concurrently; results in argument order.

def race(self, items: list) -> Any:

(index, result) of the first to finish; replay picks the same one. Losing operations are dropped (a dropped wait takes no signal).

class StepCtx:

The step function's optional argument: ctx.step("charge", lambda s: pay(key=s.key)). Built only for a function that declares it.

StepCtx(x: fusee._runtime.RunExec, seq: int, name: str, attempt: int)
seq
name
attempt
key: str

A key stable across retries and recovery for this step (distinct for each run of a workflow ID, including one that replaced it by reset). Pass it to services that accept idempotency keys.

cancelled: bool

True once the run was cancelled, terminated or closed. An async step function is cancelled at its next await; a function on a thread (thread=True) checks this to stop early.

workflow_id: str
def heartbeat(self, progress: Any = None) -> None:

Reports progress. Coalesced; never waits for the disk.

def progress(self) -> Any:

The latest progress this step reported, also from before a crash.

def stream(self, name: str) -> StreamWriter:

Opens the step's stream name: the step's result completes it, and an attempt that runs again finds what earlier attempts recorded in recorded().

log: logging.LoggerAdapter

A logger tagged with the workflow ID, run and step.

class ObjectCtx(fusee.Ctx):

The context of an object's handler: the workflow context of the key's run, its state and its object_key.

ObjectCtx(x: fusee._runtime.RunExec, exclusive: bool)
exclusive
state
object_key: str
class ObjectState:

A key's state as a handler sees it: JSON values under string keys.

ObjectState(x: fusee._runtime.RunExec, exclusive: bool)
def get(self, key: str, default: Any = None) -> Any:

The value, or default when the key has none. An exclusive handler sees its own writes.

def set(self, key: str, value: Any) -> None:

Sets a value (None clears it); committed when the handler returns.

def clear(self, key: str) -> None:
def clear_all(self) -> None:

Clears every key.

def keys(self) -> list:

The keys that hold a value, sorted.

class ObjectCall:

A key called from workflow code (ctx.object(type, key)).

ObjectCall(x: fusee._runtime.RunExec, type: str, key: str)
type
key
def call(self, handler: str, arg: Any = None) -> Any:

Calls a handler; await it for the result. A journal operation, run exactly once.

def send(self, handler: str, arg: Any = None) -> None:

Calls a handler without waiting: the outbox delivers the call once this point is durable.

class StreamWriter:

A writer of one of the run's streams (ctx.stream / s.stream).

StreamWriter( x: fusee._runtime.RunExec, name: str, step: int | None, recorded: int, closed: bool, seq: Callable[[], int | None])
recorded_count
closed
def append(self, value: Any) -> None:

Appends a chunk: bytes as bytes, anything else as JSON. Never waits for the disk.

def close(self) -> None:

Completes the stream: readers end once they have every chunk.

def recorded(self) -> list:

The chunks the step's earlier attempts recorded (after a crash, the durable ones): continue after them, so readers see no chunk twice.

@dataclass
class Completion:

What ctx.await_completion's send function receives: the token to complete with (engine.complete(token, value)), and the callback URL when the engine serves its HTTP API with a public address.

Completion(token: str, url: str | None = None)
token: str
url: str | None = None
@dataclass
class Failure:

A failure as recorded: what a failed step, update, object call or workflow carries, in every SDK. type is the error's class name (absent for a plain Exception); code and details are the application's; retryable=False marks it final.

Failure( message: str, type: str | None = None, code: str | None = None, details: Any = None, retryable: bool | None = None, stack: str | None = None, cause: Failure | None = None)
message: str
type: str | None = None
code: str | None = None
details: Any = None
retryable: bool | None = None
stack: str | None = None
cause: Failure | None = None
def to_json(self) -> dict:
def encode(self) -> bytes:
@classmethod
def from_json(cls, v: Any) -> Failure:
@classmethod
def decode(cls, payload: Any) -> Failure:

A recorded failure: an envelope, or plain text as its message.

root: Failure

The innermost cause (this failure if it has none).

def has_type(self, name: str) -> bool:
class Fatal(builtins.Exception):

Raise (or subclass) to fail a step without retries::

class CardDeclined(fusee.Fatal):
    code = "card_declined"

raise CardDeclined("card declined", details={"last4": "4242"})

Any exception with retryable = False is final as well.

Fatal(message: str = '', *, code: str | None = None, details: Any = None)
retryable = False
code: str | None = None
details: Any = None
class FuseeError(builtins.Exception):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

FuseeError( message: str, *, code: str | None = None, cause: Failure | None = None)
code: str = 'WorkflowFailed'
cause: Failure | None = None
retryable: bool = False
message
docs: str
class NotFoundError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

NotFoundError(message: str, *, id: str | None = None)
code = 'NotFound'
id
class AlreadyExistsError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

AlreadyExistsError(message: str, *, id: str | None = None, run_id: int | None = None)
code = 'AlreadyExists'
id
run_id
class UnknownWorkflowError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

UnknownWorkflowError( message: str, *, type: str | None = None, registered: list | None = None)
code = 'UnknownWorkflow'
type
registered
class StepError(fusee.FuseeError):

A step failed after its last attempt (or with a final failure). The step's own failure is cause: e.cause.type, e.cause.code, e.cause.details.

StepError( step: str, cause: Failure, *, workflow_id: str | None = None, attempts: int | None = None)
code = 'StepFailed'
step
workflow_id
attempts
retryable = False
class WorkflowError(fusee.FuseeError):

A workflow, update or object handler failed; its failure is cause.

WorkflowError(cause: Failure, *, workflow_id: str | None = None)
code = 'WorkflowFailed'
workflow_id
class ChildError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

ChildError(workflow_id: str, cause: Failure)
code = 'ChildFailed'
workflow_id
retryable = False
class CancelledError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

CancelledError(reason: str = 'cancelled')
code = 'Cancelled'
retryable = True
reason
class TimeoutError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

TimeoutError(message: str = 'timed out')
code = 'TimedOut'
retryable = True
class TerminatedError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

TerminatedError(reason: str = 'terminated')
code = 'Terminated'
reason
class DeterminismError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

DeterminismError(message: str, *, workflow_id: str | None = None)
code = 'Determinism'
workflow_id
class RejectedError(fusee.FuseeError):

A request was refused and nothing was recorded.

RejectedError(message: str)
code = 'Rejected'
reason
class CodecError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

CodecError(message: str, *, step: str | None = None, path: str | None = None)
code = 'Codec'
step
path
class ConnectionError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

ConnectionError(message: str, *, address: str | None = None)
code = 'Connection'
retryable = True
address
class IoError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

code = 'Io'
retryable = True
class BusyError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

code = 'Busy'
retryable = True
class ClosedError(fusee.FuseeError):

Every error Fusee raises. code is the same in every SDK; docs links to its page.

ClosedError(message: str = 'engine closed')
code = 'Closed'
DOCS_BASE = 'https://github.com/bitsinfra/fusee/blob/main/docs/errors.md'
class Replayer(fusee._runtime.CodeHost):

Runs workflow code against journals exported with fusee export (or engine.export_journal), without running step functions or timers, and reports the first difference by step name::

report = await fusee.Replayer(workflows=[checkout]).replay_file("order-1.fjnl")
assert report.ok, report.error
Replayer( workflows: Iterable[Workflow] = (), *, codec: list | None = None)
async def replay(self, file: bytes) -> ReplayReport:
async def replay_file(self, path: str) -> ReplayReport:
async def replay_dir(self, dir: str) -> list:

Replays every .fjnl file in a directory.

@dataclass
class ReplayReport:
ReplayReport( workflow_id: str, workflow_type: str, ok: bool = False, error: str | None = None, finished: bool = False, file: str | None = None)
workflow_id: str
workflow_type: str
ok: bool = False
error: str | None = None
finished: bool = False
file: str | None = None
class Telemetry:

OpenTelemetry instruments for an engine: a fusee.step span per step attempt, current while the step's function runs, and the metrics fusee.steps.executed, fusee.step.duration (ms), fusee.workflows.started and fusee.workflows.finished. Replayed steps record nothing. Build one with open_telemetry(), or pass any tracer and meter with the OpenTelemetry API's methods.

Telemetry( tracer=None, meter=None, use_span=None, status=None, traceparent=None, remote=None, span_in=None)
def open_telemetry() -> Telemetry:

Instruments from the application's opentelemetry package (the API; the application configures providers and exporters).

def aes_gcm(keys: dict) -> list:

Payload encryption with AES-256-GCM: aes_gcm({"k2": new_key, "k1": old_key}). Keys are 32 bytes (or hex / base64 text). The first key encrypts; every key decrypts.

def duration_ms(d: None | int | float | str | datetime.timedelta) -> float | None:

A duration in milliseconds: a number (ms), "500ms", "10s", "5m", "2h", "1d", or a timedelta.

class InvocationHandler:

The worker side of invocations over HTTP (suspend to zero).

When a Fusee server started with --invoke URL has work for shards no worker holds, it POSTs to that URL; each request opens a fresh engine for the invocation's shards, runs them until they are idle or the budget ends, and closes it. The handler is an ASGI app (uvicorn module:app); lambda_handler is an AWS Lambda handler; invoke serves any other framework. An engine belongs to the thread that opened it: run invocations on one thread (Lambda and ASGI servers do)::

app = invocation_handler("fusee.internal:7600?token-file=/run/token",
                         workflows=[order],
                         secret=os.environ["FUSEE_INVOKE_SECRET"])

register(engine), if given, runs after the engine opens (for setup beyond the definitions).

InvocationHandler( address: str, *, workflows=(), objects=(), register: Callable[[Engine], Any] | None = None, secret=None, **engine_options)
address
secret
workflows
objects
register
engine_options
async def invoke(self, headers: dict | None, body: bytes) -> tuple:

Verifies and runs one invocation: (status, headers, body).

def lambda_handler(self, event: dict, context=None) -> dict:

An AWS Lambda handler (function URL, API Gateway or ALB proxy events).

def invocation_handler( address: str, *, workflows=(), objects=(), register: Callable[[Engine], Any] | None = None, secret=None, **engine_options) -> InvocationHandler:
def verify_signature( secret, header: str, body: bytes, now_ms: int | None = None) -> str | None:

Checks a fusee-signature value (t=<unix ms>,v1=<hex HMAC-SHA256>) within five minutes. Returns why it fails, or None.

SIGNATURE_HEADER = 'fusee-signature'