Skip to content

feat(python): port dynamic-workflows to Python and add playground example#5

Draft
LuisDuarte1 wants to merge 1 commit into
cloudflare:mainfrom
LuisDuarte1:lduarte/dynamic-workflows-python
Draft

feat(python): port dynamic-workflows to Python and add playground example#5
LuisDuarte1 wants to merge 1 commit into
cloudflare:mainfrom
LuisDuarte1:lduarte/dynamic-workflows-python

Conversation

@LuisDuarte1
Copy link
Copy Markdown

NOTE: this is still a very clanker PR, also for workspaces to work in uv I had to patch pywrangler cloudflare/workers-py#107

Adds a Python port of @cloudflare/dynamic-workflows alongside the existing JS package, and an interactive browser playground example mirroring examples/basic.

packages/dynamic-workflows-py/:

  • Pure-Python _core module exporting the envelope helpers (_wrap_params, _unwrap_params, MissingDispatcherMetadataError), dispatcher_binding_impl, and dispatch_workflow_core. No js / workers / pyodide.ffi imports so the helpers are host-testable.
  • _workerd module with the workerd/Pyodide-bound layer: DynamicWorkflowBinding and DynamicWorkflowInstanceStub (WorkerEntrypoint subclasses, since RpcTarget has no documented Python subclassing path), wrap_workflow_binding, create_dynamic_workflow_entrypoint, dispatch_workflow, and WrappedWorkflow / WrappedInstance tenant facades.
  • __init__.py falls back to the _core-only surface when imported outside the Pyodide runtime, so host pytest doesn't need import shims.
  • 34 host pytest tests covering envelope wrap/unwrap, the binding-impl contract (mirrors JS binding.test.ts), and the dispatch-core flow (mirrors JS entrypoint.test.ts).

examples/python/:

  • Interactive playground dispatcher with the same UX as examples/basic: editor + payload + step timeline + status polling. SSE log streaming is omitted (no Python streaming-tail story yet); progress is driven by polling /api/status/:runId.
  • Demonstrates both trigger shapes, switchable in the dashboard UI:
    • tenant: dispatcher RPCs into the tenant's Default.start_workflow(), which calls env.WORKFLOWS.create() from the tenant's own context. Models the multi-tenant SaaS case where the tenant code is what triggers workflows.
    • direct: dispatcher calls wrap_workflow_binding(metadata).create() directly. The tenant only needs TenantWorkflow(WorkflowEntrypoint) — no Default(WorkerEntrypoint) required.
  • Per-run tenant source ships with the dispatcher metadata so workflow replays survive isolate recycles without a Durable Object.
  • Verified end-to-end against pywrangler dev: both modes complete the full Dashboard → dispatcher → tenant → @step.do chain.

Workspace plumbing:

  • Repo-root pyproject.toml declares a uv workspace with both Python packages as members.
  • examples/python/pyproject.toml resolves dynamic-workflows via [tool.uv.sources] dynamic-workflows = { workspace = true }. The workers-py and workers-runtime-sdk deps are pinned to a fork commit that patches pywrangler sync to honor uv workspaces / sources (upstream PR feat(pywrangler): honor [tool.uv.sources] and [tool.uv.workspace] via uv export workers-py#107). No pre-built wheels or find-links config needed.
  • .gitignore updated with the standard Python / uv / pywrangler / pytest / linter artifacts.

…mple

Adds a Python port of @cloudflare/dynamic-workflows alongside the existing
JS package, and an interactive browser playground example mirroring
examples/basic.

`packages/dynamic-workflows-py/`:

  * Pure-Python `_core` module exporting the envelope helpers
    (`_wrap_params`, `_unwrap_params`, `MissingDispatcherMetadataError`),
    `dispatcher_binding_impl`, and `dispatch_workflow_core`. No `js` /
    `workers` / `pyodide.ffi` imports so the helpers are host-testable.
  * `_workerd` module with the workerd/Pyodide-bound layer:
    `DynamicWorkflowBinding` (a single `WorkerEntrypoint` subclass minted
    by `wrap_workflow_binding`), `create_dynamic_workflow_entrypoint`,
    `dispatch_workflow`, and `WrappedWorkflow` / `WrappedInstance` tenant
    facades. `create()` returns a plain JS object literal
    `{id, status, pause, resume, terminate, restart, sendEvent}` with
    methods bound to the underlying JS WorkflowInstance via
    `Function.prototype.bind` — sync `.id`, RPC-callable method
    handles, no second WorkerEntrypoint class, no factory ceremony for
    instance handles.
  * `__init__.py` falls back to the `_core`-only surface when imported
    outside the Pyodide runtime, so host pytest doesn't need import shims.
  * 40 host pytest tests covering envelope wrap/unwrap, the binding-impl
    contract (mirrors JS `binding.test.ts`), and the dispatch-core flow
    (mirrors JS `entrypoint.test.ts`).

`examples/python/`:

  * Interactive playground dispatcher with the same UX as
    `examples/basic`: editor + payload + step timeline + status polling.
    SSE log streaming is omitted (no Python streaming-tail story yet);
    progress is driven by polling `/api/status/:runId`.
  * Demonstrates **both** trigger shapes, switchable in the dashboard UI:
      - **tenant**: dispatcher RPCs into the tenant's
        `Default.start_workflow()`, which calls `env.WORKFLOWS.create()`
        from the tenant's own context. Models the multi-tenant SaaS case
        where the tenant code is what triggers workflows.
      - **direct**: dispatcher calls `wrap_workflow_binding(metadata).create()`
        directly. The tenant only needs `TenantWorkflow(WorkflowEntrypoint)`
        — no `Default(WorkerEntrypoint)` required.
  * Per-run tenant source ships with the dispatcher metadata so workflow
    replays survive isolate recycles without a Durable Object.
  * Verified end-to-end against `pywrangler dev`: both modes complete the
    full Dashboard → dispatcher → tenant → @step.do chain.

Workspace plumbing:

  * Repo-root `pyproject.toml` declares a uv workspace with both Python
    packages as members.
  * `examples/python/pyproject.toml` resolves `dynamic-workflows` via
    `[tool.uv.sources] dynamic-workflows = { workspace = true }`. The
    `workers-py` and `workers-runtime-sdk` deps are pinned to a fork
    commit that patches `pywrangler sync` to honor uv workspaces /
    sources (upstream PR cloudflare/workers-py#107). No pre-built
    wheels or `find-links` config needed.
  * `.gitignore` updated with the standard Python / uv / pywrangler /
    pytest / linter artifacts.
@LuisDuarte1 LuisDuarte1 force-pushed the lduarte/dynamic-workflows-python branch from 76c7e25 to e8aac9d Compare May 19, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant