Skip to main content

fusee (Python)

Durable execution at the speed of the disk, embedded in your process (asyncio). Python 3.9 or later; typed (py.typed).

import asyncio
import fusee


@fusee.workflow("greet")
async def greet(ctx: fusee.Ctx, name: str) -> str:
    greeting = await ctx.step("compose", lambda: f"hello {name}")
    await ctx.sleep("pause", "1s")
    return greeting


async def main():
    async with await fusee.Engine.open("./data", workflows=[greet]) as engine:
        run = await engine.start(greet, "greet-1", "world")
        print(await run.result())


asyncio.run(main())

Kill the process during the sleep and run it again: the run resumes where it was, and compose does not run twice.

Definitions

Workflows, signals, queries, updates and object types are plain values, made apart from any engine, so a module can define them and a client can import them for their names and types alone:

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


@fusee.workflow("checkout")
async def checkout(ctx: fusee.Ctx, order: Order) -> Receipt:   # Order: dataclass, pydantic, msgspec, TypedDict
    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))
    ctx.on_query(status, lambda _: {"phase": "review"})
    approved = await ctx.wait_for(review, timeout="1d")             # bool | None
    if not approved:
        await ctx.step("refund", lambda: payments.refund(charge))
    return Receipt(order, charge)


engine = await fusee.Engine.open("./data", workflows=[checkout])
run = await engine.start(checkout, "order-123", order)              # Run[Receipt]
await run.signal(review, True)
receipt = await run.result()                                        # a Receipt again
  • @fusee.workflow("name") (or bare @fusee.workflow, named after the function), with version=, timeout=, input=, output=. The input's and result's annotations are read once and decide what the code receives and what run.result() returns: dataclasses, pydantic models, msgspec structs, datetime, UUID, enums, and lists and dicts of them. A start whose input does not match its type is refused before anything is recorded.
  • fusee.signal(name, type), fusee.query(name, arg=, result=), fusee.update(name, arg=, result=). A plain string works wherever a definition does, for calls across languages.
  • fusee.object("cart", {"add": add, "items": fusee.shared(items)}), or cart = fusee.object("cart") with @cart.exclusive and @cart.shared.
  • Engine.open(workflows=[...], objects=[...]) registers them; engine.register(...) adds more later. Registering a type twice is an error that names both places.

The step context

A step function may take one argument, the step context s:

s.key an idempotency key, stable across retries and recovery
s.attempt 1, 2, ...
s.heartbeat(p), s.progress() progress, also from before a crash
s.stream(name) the step's own stream
s.log a logger tagged with the workflow ID, run and step

It is built only when the function declares the parameter (lambda s: ...); lambda: ... costs nothing. Cancellation of the workflow, its deadline and the step's own timeouts cancel the step's coroutine.

A synchronous step function runs on the event loop and holds every other run while it runs: make it async, or pass thread=True to run it on a thread (or open the engine with sync_steps="thread"). Under the dev profile a synchronous step that holds the loop over 50 ms is reported once with both fixes.

Errors and retries

A failed step's error reaches the caller whole: the step's name, and its failure with the exception's type, code, details, stack and cause.

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

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

try:
    await run.result()
except fusee.StepError as e:
    e.step, e.cause.type, e.cause.code, e.cause.details   # "charge", "CardDeclined", ...
  • A failure is final when it is a fusee.Fatal, when the exception has retryable = False, when its type (or a cause's) is in Retry(non_retryable=(...)), or when Retry(retry_if=lambda failure, attempt: ...) says so. Retry(attempts=3, initial="100ms", factor=2, max="10s", jitter=True, max_elapsed=None) is the default policy.
  • A step result JSON cannot encode is a CodecError naming the path of the value (a set at $.items[1].tags), and is not retried.
  • Every error has code (the same in every SDK) and docs, a link to its page in docs/errors.md: NotFoundError, AlreadyExistsError, UnknownWorkflowError, StepError, WorkflowError, ChildError, CancelledError, TimeoutError, TerminatedError, DeterminismError, RejectedError, CodecError, ConnectionError, IoError, BusyError, ClosedError.
  • Engine.open(record_stacks=False) keeps stack traces out of the journal.

Mistakes fail where they are made: starting a type no code is registered for raises UnknownWorkflowError with the registered types and the nearest name; a run whose code no longer matches its journal logs one line with the difference and the three fixes (and engine.on("stuck", fn) is called); a run that fails keeps its whole journal for run.describe() and run.retry().

The engine and the client

Engine (embedded) and Client (through a Fusee server) have the same interface, so code that starts, signals and reads runs does not know which it holds:

start(wf, id, input, id_policy=, timeout=, version=) a Run; id_policy={"running": "use-existing"} attaches to a running ID
run(id, wf=None) a handle; wf types its result
signal_with_start(wf, id, input, signal, value) a Run; starts it if it is not running, and the signal is recorded before its code first runs
update_with_start(wf, id, input, update, arg) (run, result)
object(type, key) call(handler, arg, id=), send, state(), describe(), delete(); set_state(field, value), patch_state(ops), clear_state(field=None), edit_state(edit, if_version=)
schedule(name, spec, wf, input) spec: a cron string, {"every": "1h"}, or a ScheduleSpec; schedules.get(name), schedules.list()
list(type=, status=, started_after=, started_before=, attributes=) an async iterator of RunSummary
complete(token, value), fail(token, failure) async completions
debounce(wf, key, input, window=, max_wait=, event_id=), batch(wf, key, event, max_events=, max_wait=, event_id=) queued starts
close() also async with

A Run has signal(sig, value), query(q, arg), update(u, arg, id=), result(), status() (a RunSummary), describe() (the summary and the journal), cancel(reason), terminate(reason), reset(before_step), retry() (from the failed step), resume() (a run paused by a step whose retries ran out: Retry(on_exhausted="pause"), or Engine.open(..., on_exhausted="pause") for every step), stream(name, from_=) and started().

The engine alone has limit(name, concurrent=, per_second=), limits(), limit_usage(name), on(event, fn), metrics(), shard_metrics(), serve(addr, token=) (the HTTP API and the UI) and export_journal(id).

For code without an event loop (Django, Flask, scripts), fusee.sync.Client has the same methods, blocking: Client.connect(address), then client.start(...), run.result(timeout=30), run.signal(...).

Engine.open() with no directory reads FUSEE_DIR, else FUSEE_URL (a worker of a server), else ./fusee-data; so one program runs embedded on a laptop and against a server in production.

The dev profile

Engine.open(profile="dev") or FUSEE_PROFILE=dev (never inferred) says so on stderr, keeps every run's history, flushes with fsync instead of F_FULLFSYNC on macOS (a process crash loses nothing; power loss may), reports synchronous steps that hold the event loop, replays each run against the code when it parks or ends and logs any difference (strict replay), makes result() of a stuck run raise DeterminismError at once instead of waiting, shows ctx.log records on stderr when logging is not configured, and serves the UI (FUSEE_HTTP, else port 7601, else a free port; http="off" serves nothing).

Testing

from fusee.testing import TestEngine

t = await TestEngine.open(workflows=[checkout])
t.mock_step(checkout, "charge", lambda attempt: {"id": "ch_1"})
run = await t.engine.start(checkout, "order-1", order)
assert await t.steps("order-1") == ["quote", "charge"]
await t.check()   # strict replay of every run

See docs/testing-workflows.md.

Workflow code

Workflow coroutines run under Fusee's deterministic scheduler and may only await ctx operations; awaiting anything else is an error that says to put it in a step. ctx has step, sleep(name, duration), sleep_until(name, at), wait_for(signal, timeout=), child(wf, id, input, parent_close=, timeout=), start_child, signal_workflow(id, sig, value), cancel_workflow(id, reason=), object(type, key), on_query, on_update(update, handler, validate=), await_completion(name, send) (send(cb) gets cb.token and, when the engine serves its API, cb.url), compensate(name, fn) (a step that runs, newest first, if the workflow fails or is cancelled), continue_as_new, patched, deprecate_patch, set_attributes, stream, now, random, uuid, all, race, log and journal_size. Durations are milliseconds, "500ms", "10s", "5m", "2h", "1d", or a timedelta. Payloads are JSON (no pickle), except bytes, recorded raw.

Steps waiting for a limit's permit (limit=) go by priority (higher first), then take turns by fairness_key (fairness_weight permits per turn). See docs/features.md for streams, objects, debounce, batching, schedules, patching, encryption (Engine.open(codec=fusee.aes_gcm(keys))), telemetry (telemetry=fusee.open_telemetry()) and replay testing (fusee.Replayer(workflows=[...]).replay_file(path)).

Agents

fusee.ai.model_call makes a model call a step whose tokens stream to readers. Agents written with a framework become durable without changing their loop: each model call and each tool call is a step, so a crash resumes at the call that was running.

from fusee.ai.pydantic import FuseeDurability, run      # pip install 'fusee[pydantic-ai]'

agent = Agent("openai:gpt-5", name="support", tools=[refund], capabilities=[FuseeDurability()])

@fusee.workflow("support")
async def support(ctx, question):
    return (await run(ctx, lambda: agent.run(question))).output

fusee.ai.openai.run(ctx, agent, question) does the same for the OpenAI Agents SDK (pip install 'fusee[openai-agents]'). See Agents and Agent patterns.

Server and replicated modes

engine = await fusee.Engine.open(durability="server", address="127.0.0.1:7600", workflows=[checkout])

client = await fusee.Client.connect("127.0.0.1:7600", token_file="/etc/fusee/token",
                                    tls={"ca": "/etc/fusee/ca.pem"})
run = await client.start(checkout, "order-42", order)

Several Python processes can run one server's workflows, each on its own core: start each worker with max-shards=N (and shm=1 for shared memory on the same host), as in address="unix:/run/fusee.sock?shm=1&max-shards=2". Each takes the free shards it may hold and runs only their workflows; all of them append to the server's one log. A worker's own engine.start or engine.run(id) works for IDs of its own shards only; use a Client for the others. For replication, start a worker on each of three or five machines with durability="replicated", replica_id, peers and dir.

Invoked workers (scale to zero)

A server started with --invoke URL POSTs to that URL when a shard that no worker holds has work due. invocation_handler answers: each request opens a fresh engine for the invocation's shards, runs them until they are idle or the budget ends, and closes it.

app = fusee.invocation_handler(
    "fusee.internal:7600?ca=/etc/fusee/ca.pem&token-file=/etc/fusee/token",
    workflows=[order],
    secret=os.environ["FUSEE_INVOKE_SECRET"],  # the server's --invoke-secret-file
)
# ASGI: uvicorn module:app;  AWS Lambda: app.lambda_handler

Building

maturin develop in this directory, or cargo build --release -p fusee-python and copy the library to python/fusee/_native.abi3.so (on macOS remove the old file first: overwriting a loaded library in place invalidates its signature).

Metadata

Release files for fusee 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for fusee 1.0.0
File Actions
fusee-1.0.0-cp39-abi3-musllinux_1_2_x86_64.whl CPython 3.9 abi3 Linux musl 1.2+ x86-64 Details for fusee-1.0.0-cp39-abi3-musllinux_1_2_x86_64.whl
fusee-1.0.0-cp39-abi3-musllinux_1_2_aarch64.whl CPython 3.9 abi3 Linux musl 1.2+ ARM64 Details for fusee-1.0.0-cp39-abi3-musllinux_1_2_aarch64.whl
fusee-1.0.0-cp39-abi3-manylinux_2_28_x86_64.whl CPython 3.9 abi3 Linux glibc 2.28+ x86-64 Details for fusee-1.0.0-cp39-abi3-manylinux_2_28_x86_64.whl
fusee-1.0.0-cp39-abi3-manylinux_2_28_aarch64.whl CPython 3.9 abi3 Linux glibc 2.28+ ARM64 Details for fusee-1.0.0-cp39-abi3-manylinux_2_28_aarch64.whl
fusee-1.0.0-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details for fusee-1.0.0-cp39-abi3-macosx_11_0_arm64.whl
fusee-1.0.0-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details for fusee-1.0.0-cp39-abi3-macosx_10_12_x86_64.whl

Total release size: 184.7 MB

Release files / fusee-1.0.0-cp39-abi3-musllinux_1_2_x86_64.whl

Download URL fusee-1.0.0-cp39-abi3-musllinux_1_2_x86_64.whl
Size 40.3 MB
Tags CPython 3.9 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
43b47d48a4e1cab457a385585a8c4c68fc2e08f1ecadf7f8ec9ac40cd8229f65
BLAKE2b-256 checksum
How to use checksums
9fe8097d2f437e1bb7260c2ea29432bbd94b60573039795357f3b69cf1badbb3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Release files / fusee-1.0.0-cp39-abi3-musllinux_1_2_aarch64.whl

Download URL fusee-1.0.0-cp39-abi3-musllinux_1_2_aarch64.whl
Size 39.7 MB
Tags CPython 3.9 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
6a612b7c600ebf3f1d4671f442166d66f62ebaeeb411f0e9db6583ceebe8c978
BLAKE2b-256 checksum
How to use checksums
c9725349ac7b0ac99e18a6cbebde3b057e8a23135004c9c8c4b34d916e521357
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Release files / fusee-1.0.0-cp39-abi3-manylinux_2_28_x86_64.whl

Download URL fusee-1.0.0-cp39-abi3-manylinux_2_28_x86_64.whl
Size 42.0 MB
Tags CPython 3.9 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
2621afd5f0efb04ff6f6ec73ab4dcf5723241402db829db68c968aa5ffdbb4c3
BLAKE2b-256 checksum
How to use checksums
05396cfcf495cde87a0ac00d8abd89aeff44e407a36b57778a16116bbec91811
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Release files / fusee-1.0.0-cp39-abi3-manylinux_2_28_aarch64.whl

Download URL fusee-1.0.0-cp39-abi3-manylinux_2_28_aarch64.whl
Size 39.5 MB
Tags CPython 3.9 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
70789b9a739c70542016ad42fdb9193a7267cc5d39b9f64589da30472ea4b62c
BLAKE2b-256 checksum
How to use checksums
ce0886a01d6b0dc5d3a34c0b7e0acba4be06e0005e871238e8eea0c329cc28f9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Release files / fusee-1.0.0-cp39-abi3-macosx_11_0_arm64.whl

Download URL fusee-1.0.0-cp39-abi3-macosx_11_0_arm64.whl
Size 11.3 MB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
91170817cd7d6fce11f8a363af21f985e6b495053fbec8b2b913e4131f3fadea
BLAKE2b-256 checksum
How to use checksums
d5c74c6eff780c27af133115e6cdf33ece9d73f55a7443d9ab3c86c6822153f4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Release files / fusee-1.0.0-cp39-abi3-macosx_10_12_x86_64.whl

Download URL fusee-1.0.0-cp39-abi3-macosx_10_12_x86_64.whl
Size 11.8 MB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
bc8aff333e721bcdfeb8beb1c151c68aa19a86af56c3d3e0f6b00f3440e27edc
BLAKE2b-256 checksum
How to use checksums
18610e11d4f2eef582dc0bd40a5b7b47e310149961774883e533ca9792699c78
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

Security for fusee 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 6 files in this release have been uploaded with provenance metadata.

Select from 6 files

Attestation for fusee-1.0.0-cp39-abi3-musllinux_1_2_x86_64.whl

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 12, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

6 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page