Skip to main content

Dex SDK for Python

Python SDK for Dex workflow engine

New user contracts

The rewrite targets Python 3.11+ and exposes strongly typed workflow contracts from dex. This phase includes definitions, attributes, channels, streams, waits, decisions, codecs, registry validation, synchronous client calls, and synchronous worker handlers. Python owns its gRPC Client and Worker transport; the shared Rust Core is used only for BlobCache.

from datetime import timedelta

import dex

counter = dex.Attribute("counter", int)
counters_by_region = dex.AttributeMap("counters-by-region", int)
progress = dex.Stream("progress", str, 10 * 1024 * 1024)

class Run(dex.Step[str]):
    def wait_for(
        self, context: dex.Context, input: str
    ) -> dex.Wait:
        return dex.Wait.until(
            dex.Timer.by_duration(timedelta(seconds=1))
        )

    def execute(
        self, context: dex.Context, input: str
    ) -> dex.StepDecision:
        progress.write(context, "running")
        return dex.graceful_complete(input)

class CounterFlow(dex.Flow[str]):
    run = Run()

    def get_flow_type(self) -> str:
        return "Counter"

    def get_steps(self) -> dex.StepList[str]:
        return dex.StepList.start_step(self.run)

    def get_persistence_schema(self) -> dex.PersistenceSchema:
        return dex.PersistenceSchema.of(counter, counters_by_region, progress)

    @dex.rpc(name="Increment")
    def increment(
        self, context: dex.Context, input: int
    ) -> dex.RPCResult[int]:
        return dex.RPCResult(input + 1)

flow = CounterFlow()
registry = dex.Registry((flow,))

Registry derives codecs from declared Python types and handler annotations. Built-in primitive types and dataclasses need no codec arguments. Register an explicit codec only for a custom encoding or a type Registry cannot derive. PersistenceSchema.of(...) accepts attributes, channels, and streams together and partitions them by definition type.

Streams provide best-effort resumable progress messages. Their approximate byte budget is shared by all instances of the owning Flow type. Client keys cannot contain #; Step writes generate runID#stepExecutionID and allow one write per Stream per invocation.

client.write_stream(flow_id, progress, "frontend/1", "starting")
message = client.read_stream(
    flow_id, progress, resume_token, timeout=timedelta(seconds=30)
)
resume_token = message.resume_token

Async Step handlers await Stream.write, and AsyncClient exposes matching async methods. Reads return the decoded value, resume token, creation time, and idempotency key.

Worker and AsyncWorker synchronize all registered Indexed Attributes with Dex Server before opening their listener. Existing indexes return immediately; failure or the default two-minute deadline aborts startup. An indexed AttributeMap must provide one fixed index_key.

Initial attributes retain their value types without a public wrapper class:

options = (
    dex.StartFlowOptions()
    .with_attribute(counter, 1)
    .with_attribute(counters_by_region, "us-west", 1)
)

Opt in when declaring an Attribute or AttributeMap, and select the Store in Flow configuration:

email = dex.Attribute("customer-email", str, sync_to_attribute_store=True)
config = dex.FlowConfig(attribute_store_names=["profiles", "audit"])

Stores are asynchronous latest-state projections. Every enabled Attribute write is sent to every selected Store. Deletion writes SQL NULL, and projection failures do not roll back Flow Attributes. None preserves current targets; an explicit empty list disables future synchronization while retaining protocol presence.

pip install dex-python-sdk==0.1.0

See samples for use case examples.

Requirements

Concepts

Applications implement two generic interfaces from dex:

  • Flow[START_INPUT] returns StepList.start_step(...), followed by optional .other_steps(...), from one get_steps() method. The StepList generic binds the Flow input to the starting Step input. Use StepList.empty() when a Flow has no Steps.
  • Step[INPUT] implements execute and optionally wait_for. The default Worker path requires synchronous handlers. With AsyncWorker and Registry(..., allow_async_handlers=True), handlers may be async def and await an AsyncClient.

StepOptions.wait_for_method_timeout and execute_method_timeout bound the two handler calls. Timer and channel conditions determine how long a Step waits.

wait_for_retry and execute_retry limit one logical handler execution. With StepDurability.ASYNC, local and fallback regular activities share maximum attempts, total duration, and 1-based attempt numbers. Fallback starts immediately; later regular retries continue the backoff sequence at the cumulative attempt.

Canceling Step executions

A successful Step can cancel queued or active executions while continuing with its normal decision:

return (
    dex.go_to(RecordQuote, quote)
    .with_canceling_sibling_steps(QuoteCarrierA, QuoteCarrierB)
    .with_canceling_steps(GlobalQuoteTimeout)
)

with_canceling_steps selects every current execution of each registered Step type. with_canceling_sibling_steps selects only executions with the same Context.from_step_execution_id as the current execution. Decisions are immutable; repeated calls form a union, and Flow-wide selection wins for the same Step type. Unregistered selectors produce an invalid Step result.

Dex resolves one snapshot after the current execution succeeds. Completed, already-canceled, and absent targets are no-ops. Next Steps created by the same decision are outside the snapshot. Dex immediately applies the next or close action; late decisions, writes, retries, and recovery Steps are discarded.

Set StepOptions.heartbeat_timeout on long-running regular Steps so cancellation reaches the Worker promptly. It applies to wait_for and execute; local activities ignore it, while an ASYNC fallback uses it. None and zero disable heartbeats, and positive values must be whole seconds in the signed int32 range. AsyncWorker cancels the handler's asyncio task. A handler may catch asyncio.CancelledError for cleanup; synchronous CPU-bound handlers may check Context.is_cancellation_requested() at natural boundaries.

RPCResult.with_canceling_steps provides the Flow-wide selector for RPCs. RPCs do not support sibling selection because they have no Step execution lineage.

Soft Flow timeout

Override Flow.handle_timeout to make a positive timeout use handler policy by default. Both synchronous and async Workers support the hook:

class Orders(dex.Flow[str]):
    async def handle_timeout(self, context: dex.Context) -> dex.StepDecision:
        await notify_expiration(context)
        return dex.force_complete("expired")

options = dex.StartFlowOptions(
    timeout=timedelta(minutes=30),
    timeout_policy=dex.FlowTimeoutPolicy.HANDLER,
)

Register async hooks with allow_async_handlers=True and run them with AsyncWorker. FAIL produces FlowErrorType.FLOW_TIMEOUT and permits Flow retry; CANCEL cancels without retry. Continue-as-new preserves the deadline, while retry runs receive a fresh budget. A zero or absent timeout disables the feature.

Registry validates every Flow, Step, RPC signature, durable name, lock, and codec before Client or Worker startup. Client methods use these typed objects instead of raw Flow, Step, or RPC strings.

Waiting and map inspection

Wait.all_of and Wait.any_of may use unnamed Conditions. Every Condition in Wait.any_combination_of must have a non-empty user ID; the same Condition instance may appear in multiple combinations.

Both Client and AsyncClient provide singleton and AttributeMap-instance overloads of wait_for_attribute_equal. They target the current run and accept only string, bool, int, or float wire values. JSON objects, bytes, and null fail before transport. AttributeMap.get_map_size/get_all_instance_keys include buffered sets and deletes. The matching ChannelMap methods are RPC-only, include buffered publishes, and omit empty instances. Keys are decoded and sorted. Use force_complete_if_channels_empty(...) for conditional completion.

Client.wait_for_flow and AsyncClient.wait_for_flow return a FlowResult after hydrating every output-bearing completion. Use single_output only when the Flow contract produces exactly one output:

output = client.wait_for_flow(flow_id).single_output(OrderResult)

result = client.wait_for_flow(flow_id)
for completion in result.completions:
    if completion.step_execution_id == expected_execution_id:
        output = completion.decode(OrderResult)

completions is an immutable tuple in server collection order. Parallel branch order is not deterministic, so select by step_type or step_execution_id. No-output Flows return an empty tuple; single_output raises ValueError for zero or multiple completions. Every terminal status returns a FlowResult; inspect status, error_type, and error_message for unsuccessful completion.

SubFlows are normal, independently addressable Flows used as durable Conditions:

def wait_for(self, context: Context, input: ChargeInput) -> Wait:
    return Wait.until(SubFlow.run(self.charge_flow, input))

def execute(self, context: Context, input: ChargeInput) -> StepDecision:
    del input
    receipt = SubFlow.get_condition_results(context).single_output(Receipt)
    return graceful_complete(receipt)

SubFlow.get_flow_id(context, index=0) remains available for a running any_of loser. SubFlowOptions configures timing, timeout policy, retry, initial target Attributes, Flow config, Condition ID, and reuse. Parent completion does not cancel an unfinished SubFlow.

Errors

Client calls raise concrete DexServiceError subclasses. Existing-Flow reads (get_attribute, describe_flow, wait_for_flow, and time_travel) raise FlowNotFoundError when the Flow does not exist. Mutations, RPCs, timer/Step waits, config updates, and continue-as-new triggers raise FlowNotActiveError when no running Flow can accept the operation.

try:
    client.publish(flow_id, orders.approved, order_id)
except dex.FlowNotActiveError:
    # The Flow is missing or already closed.
    pass

Duplicate starts, worker failures, RPC lock contention, and long-poll timeouts raise FlowAlreadyStartedError, WorkerInvocationError, RpcLockConflictError, and LongPollTimeoutError. All service errors retain code, sub_status, detail, operation, flow_id, and the original gRPC exception through Python exception chaining. Worker failures also expose worker_code, worker_error_type, and worker_error_detail. Registration, serialization, and invalid handler returns use FlowDefinitionError, ValueMappingError, and InvalidStepResultError.

Sync vs asyncio

  • Sync (default): Client and Worker use blocking gRPC and a thread-pool Worker. Blocking Client calls inside Step.execute are safe (one pool thread is occupied; other RPCs still run).
  • Asyncio: AsyncClient and AsyncWorker use grpc.aio. Use Registry(..., allow_async_handlers=True) when Steps/RPCs are coroutines. Inside async execute, inject AsyncClient — do not call sync Client on the Worker event loop. Sync Worker still rejects coroutine handlers at registry construction unless allow_async_handlers=True (and even then the sync Worker dispatcher rejects awaitable return values).

Integration scenarios live under tests/integ. They exercise the same workflows, client operations, and assertions as the Java suite against an isolated dexcli dev environment.

Implementation status

The strongly typed contracts, registry, synchronous Client/Worker, optional AsyncClient/AsyncWorker (grpc.aio), and Rust-backed BlobCache are implemented. Python owns its gRPC transport; the native bridge is limited to the shared BlobCache. Design notes: docs/design/plan/python-sdk-async-apis.md.

Running Dex locally

Install and start the complete local environment with dexcli:

brew install superdurable/tap/dexcli
dexcli dev

Dex Server listens on 127.0.0.1:8801. See the CLI README for endpoints and persistence options.

How To Contribute

This project uses uv for Python versions, dependencies, virtual environments, locking, building, and publishing.

To install requirements:

uv sync --locked

Run the complete Python SDK integration suite with an isolated Dex development environment:

./run-integration-tests.sh

Measure integration coverage

Run the same integration suite with Python source coverage:

./run-integration-tests.sh --coverage

Only the integration scenarios contribute execution data, and only production Python modules under dex are measured. Generated protobuf modules under dex/dexpb are excluded. The terminal report lists uncovered line ranges. The browser report starts at coverage/html/index.html; coverage/coverage.xml and coverage/lcov.info are also generated.

CI uploads LCOV to Codecov with GitHub OIDC under the sdk-python-integration flag and retains the full report as the sdk-python-integration-coverage Actions artifact.

Update IDL

Edit protos/dex.proto. Rename catalog: docs/design/idl-renames.md.

Generate stubs from IDL

make -C ../protos proto-python

Checked-in Python stubs land in dex/dexpb/.

Linting

Validate that every dex.__all__ class, function, constant, public method, argument, return value, dataclass field, enum value, and public instance attribute has a Google-style docstring:

uv run --frozen python scripts/check_public_docs.py

The checker resolves definitions from the public package export table, so private helpers and generated protobuf modules are excluded. Use help(dex.Client) or IDE hover information to read the same documentation. To run all other linting for this project:

uv run --frozen pre-commit run --show-diff-on-failure --color=always --all-files

Code of Conduct

This project is governed by the Contributor Covenant v 1.4.1. (Review the Code of Conduct and remove this sentence before publishing your project.)

Publishing to PyPI

  1. Optionally run Publish Python SDK to PyPI via workflow_dispatch with a version and publish=false to validate all distributions without uploading.
  2. Create a GitHub Release with tag sdk-python/vX.Y.Z (for example sdk-python/v0.1.0). CI stamps that version into pyproject.toml for the build (same idea as the TypeScript SDK release), then builds and smoke-tests Linux x86_64/ARM64, macOS x86_64/ARM64, and Windows x86_64 wheels, verifies the source distribution, and publishes with PYPI_TOKEN.
  3. After publishing, bump the committed pyproject.toml / docs install line when you want the repo tip to reflect the released version.

A manual run publishes only from main, and only when publish is explicitly selected. The dispatch version input is stamped the same way as a release tag.

See CONTRIBUTING.md for monorepo tag conventions.

License

Super Durable Source License 1.0, with legacy portions under their original terms as described in LEGACY_NOTICES.md.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dex_python_sdk-0.2.2.tar.gz (152.7 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

dex_python_sdk-0.2.2-cp311-abi3-win_amd64.whl (523.1 kB view details)

Uploaded CPython 3.11+Windows x86-64

dex_python_sdk-0.2.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (679.2 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ x86-64

dex_python_sdk-0.2.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (676.9 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ ARM64

dex_python_sdk-0.2.2-cp311-abi3-macosx_11_0_arm64.whl (613.9 kB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

dex_python_sdk-0.2.2-cp311-abi3-macosx_10_12_x86_64.whl (635.4 kB view details)

Uploaded CPython 3.11+macOS 10.12+ x86-64

File details

Details for the file dex_python_sdk-0.2.2.tar.gz.

File metadata

  • Download URL: dex_python_sdk-0.2.2.tar.gz
  • Upload date:
  • Size: 152.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for dex_python_sdk-0.2.2.tar.gz
Algorithm Hash digest
SHA256 f15379bea44166b2ed1bd1dae45fc3a39f41075d19fbaeaddcbd1ce7b1f108a8
MD5 1ecc461c40e2e61652f83faadaee0712
BLAKE2b-256 dd839d1450e621df9df85a9bdbdf8d9d2efa2090f596f5161dce163da6c43fdd

See more details on using hashes here.

File details

Details for the file dex_python_sdk-0.2.2-cp311-abi3-win_amd64.whl.

File metadata

  • Download URL: dex_python_sdk-0.2.2-cp311-abi3-win_amd64.whl
  • Upload date:
  • Size: 523.1 kB
  • Tags: CPython 3.11+, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for dex_python_sdk-0.2.2-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 4df680f37ebf6e27d685301580d3bf985bf413f32b31aaf205cc14e2a1f9cb95
MD5 e247c2a70bc28288e3a7d75a8cb15bd9
BLAKE2b-256 d435fad8ea1e2e5f3fb69d4908c6ef494bcfce7aa37ebaa57b880506b5e773f3

See more details on using hashes here.

File details

Details for the file dex_python_sdk-0.2.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

  • Download URL: dex_python_sdk-0.2.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
  • Upload date:
  • Size: 679.2 kB
  • Tags: CPython 3.11+, manylinux: glibc 2.17+ x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for dex_python_sdk-0.2.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1b824ae12257dd8eb3a0ca993266797175e8bd75eedd0df1541f980d3ba93c1c
MD5 f7cf09c38d8c70d67c9ecf0de523575e
BLAKE2b-256 e9f7fb5decc71bdd3abbd1a588b2728deb20053d154f2ce77175a66fd866dc01

See more details on using hashes here.

File details

Details for the file dex_python_sdk-0.2.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

  • Download URL: dex_python_sdk-0.2.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
  • Upload date:
  • Size: 676.9 kB
  • Tags: CPython 3.11+, manylinux: glibc 2.17+ ARM64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for dex_python_sdk-0.2.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 33ca8cda63ab5291aad4136906a50d36904ed321b5f3cc664ab5a904f4fd76b2
MD5 ca007ae469e397625bbd2db9a7b54576
BLAKE2b-256 9803989f159bb970bf0284d0edb634adf7c52cf4b792b29f7844e29398a20b1a

See more details on using hashes here.

File details

Details for the file dex_python_sdk-0.2.2-cp311-abi3-macosx_11_0_arm64.whl.

File metadata

  • Download URL: dex_python_sdk-0.2.2-cp311-abi3-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 613.9 kB
  • Tags: CPython 3.11+, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for dex_python_sdk-0.2.2-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f0e643b4cac345428639173248c757ce66adcbb40b45a9c72dab9aa4d11f6fed
MD5 3f5fef2eb445d5314acd05f9e7827081
BLAKE2b-256 6af8c2f382f3b77128df46656685f7cfc0fa106ecd079e32d47ae89939b032a0

See more details on using hashes here.

File details

Details for the file dex_python_sdk-0.2.2-cp311-abi3-macosx_10_12_x86_64.whl.

File metadata

  • Download URL: dex_python_sdk-0.2.2-cp311-abi3-macosx_10_12_x86_64.whl
  • Upload date:
  • Size: 635.4 kB
  • Tags: CPython 3.11+, macOS 10.12+ x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for dex_python_sdk-0.2.2-cp311-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 f01bdfc76909eab29d7ed7fe20a923ac050cf39b1b9817a9fbfafe75b221bf6c
MD5 598cf7805b938a09bc17aee4c5212388
BLAKE2b-256 11405b6c3caf1ec795a03e8235c070600b28e6b114ea7a938992facba7713c27

See more details on using hashes here.

Release history Release notifications | RSS feed

0.7.0

6 files

0.6.0

6 files

0.5.0

6 files

0.4.0

6 files

0.3.2

6 files

0.3.1

6 files

0.2.11

6 files

0.2.10

6 files

0.2.9

6 files

0.2.8

6 files

0.2.7

6 files

0.2.6

6 files

0.2.5

6 files

0.2.4

6 files

0.2.3

6 files

This release

0.2.2 This release

6 files

0.2.1

6 files

0.2.0

6 files

0.1.11

6 files

0.1.10

6 files

0.1.5

6 files

0.1.4

6 files

0.1.3

6 files

0.1.2

6 files

0.1.1

6 files

0.0.2

6 files

0.0.1

2 files

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