Files
temporalio__skill-temporal-…/references/python/determinism-protection.md
Donald Pinckney b5719bc143 PR Tracking Initial Release (#4)
* Add initial skill for testing, which is simply Steve's skill (#1)

* Add initial skill for testing, which is simply Steve's skill

* Rename skill to 'temporal-dev' and update version

Updated skill name and version for Temporal Python.

* Use claude to merge Steve's, Max's, and Mason's skills.  (#2)

* Use claude to merge Steve's, Max's, and Mason's skills. Did a review pass using claude's skill devlopment skills

* Add missing things from Steve

* trigger tweaks

* Add in common gotchas from Johann

* add simple feedback mechanism (#3)

* Change skill name to kebab-case, for compatibility with Amp and Cline (#7)

* Clean up references/core/ai-integration.md

* Clean up references/core/common-gotchas.md

* Clean up references/core/common-gotchas.md

* Clean up references/core/determinism.md

* Clean up references/core/determinism.md

* Update error-reference.md

* Update interactive-workflows.md

* Clean up patterns.md

* Cut shell scripts

* Edit troubleshooting.md

* remove interceptors for now

* remove dynamic workflows

* clarify on heartbeating of async activity completions, and prompt it a bit in relation to signals

* Improve references/python/advanced-features.md

* Use explicit namespace in connect

* remove duplicated content from determinism.md, clean up

* Improve references/python/data-handling.md

* Prefer start_to_close_timeout

* don't explicitely provide defaults for retry policies

* error-handling.md cleanup

* move idempotency patterns to patterns.md

* remove multi-param activities

* small edits

* Unify sandbox stuff into one file

* local activities aren't experimental

* Clean up references/python/sync-vs-async.md

* Cleanup observability.md, remove duplicated search attributes

* Cut otel for now

* cut a lot of duplicate stuff from python gotchas, address comments

* de-duplicate content

* Lots of improvements to testing

* cleanup to top level of skill (like CLI install instructions), and to top-level of python

* Improve patterns.md

* clean up ai-patterns.md

* Update readme with installation instructions

* remove ts directory

* De-couple core from python and TypeScript as much as possible

* Remove TypeScript hints

* add prompting for feedback at startup - wait for ethan on slack channel

* shorten url

* Update slack channel

* Automated pass over on python cleanup & deduplication

* Remove multi-patching from Python, since its obvious, dont waste tokens on it. (#34)

* Add TypeScript (#31)

Adds initial support for TypeScript to the skill

---------

Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>

* Fix typos and reference links (#36)

* Fix typos and reference links

* 2 more typo fixes

* quick edit to readme (#37)

* Fix saga compensations to run under cancellation protection (#43)

When a workflow is cancelled mid-saga, compensations must run in a
cancellation-protected scope, otherwise they are immediately cancelled
before they can execute.

- Python: wrap compensation loop in asyncio.shield() so it runs even
  when the workflow receives a CancelledError
- TypeScript: wrap compensation loop in CancellationScope.nonCancellable()
  so it runs even when the root scope is cancelled (per official docs:
  "Cleanup logic must be in a nonCancellable scope")
- TypeScript: also fix compensation registration order — register BEFORE
  calling the activity (was already correct in Python)

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Update readme for public preview (#45)

* a few more readme tweaks (#46)

* Add MIT License to the project (#47)

* Add Go (supersedes other PR) (#38)

* progress on go

* Go translation workflow completed.

* missed a few spots

* Manual edits

* Address feedback

* Add gotcha about anonymous local activities

* Sample code for payload converter

* clarify sdk protection mechanisms

* Setup CODEOWNERS to AI SDK team (#48)

* Align version number in SKILL.md and plugin.json. (#49)

---------

Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:36:15 -04:00

6.8 KiB
Raw Permalink Blame History

Python Workflow Sandbox

Overview

The Python SDK runs workflows in a sandbox that provides automatic protection against non-deterministic operations. This is unique to the Python SDK.

How the Sandbox Works

The sandbox:

  • Isolates global state via exec compilation
  • Restricts non-deterministic library calls via proxy objects
  • Passes through standard library with restrictions
  • Reloads workflow files on each execution

Forbidden Operations

These operations will fail in the sandbox:

  • Direct I/O: Network calls, file reads/writes
  • Threading: threading module operations
  • Subprocess: subprocess calls
  • Global state: Modifying mutable global variables
  • Blocking sleep: time.sleep() (use workflow.sleep(timedelta(...)))

Pass-Through Pattern

Third-party libraries that aren't sandbox-aware need explicit pass-through:

from temporalio import workflow

with workflow.unsafe.imports_passed_through():
    import pydantic
    from my_module import my_dataclass

When to use pass-through:

  • Data classes and models (Pydantic, dataclasses)
  • Serialization libraries
  • Type definitions
  • Any library that doesn't do I/O or non-deterministic operations
  • Performance, as many non-passthrough imports can be slower

Note: The imports, even when using imports_passed_through, should all be at the top of the file. Runtime imports are an anti-pattern.

Importing Activities

Activities should be imported through pass-through since they're defined outside the sandbox:

# workflows/order.py
from temporalio import workflow

with workflow.unsafe.imports_passed_through():
    from activities.payment import process_payment
    from activities.shipping import ship_order

@workflow.defn
class OrderWorkflow:
    @workflow.run
    async def run(self, order_id: str) -> str:
        await workflow.execute_activity(
            process_payment,
            order_id,
            start_to_close_timeout=timedelta(minutes=5),
        )
        return await workflow.execute_activity(
            ship_order,
            order_id,
            start_to_close_timeout=timedelta(minutes=10),
        )

Disabling the Sandbox

@workflow.defn
class MyWorkflow:
    @workflow.run
    async def run(self) -> str:
        with workflow.unsafe.sandbox_unrestricted():
            # Unrestricted code block
            pass
        return "result"
  • Per‑block escape hatch from runtime restrictions; imports unchanged.
  • Use when: You need to call something the sandbox would normally block (e.g., a restricted stdlib call) in a very small, controlled section.
  • IMPORTANT: Use it sparingly; you lose determinism checks inside the block
  • Genuinely non-deterministic code still MUST go into activities.

Customizing Invalid Module Members

invalid_module_members includes modules that cannot be accessed.

Checks are compared against the fully qualified path to the item.

import dataclasses
from temporalio.worker import Worker
from temporalio.worker.workflow_sandbox import (
  SandboxedWorkflowRunner,
  SandboxMatcher,
  SandboxRestrictions,
)

# Example 1: Remove a restriction on datetime.date.today():
restrictions = dataclasses.replace(
    SandboxRestrictions.default,
    invalid_module_members=SandboxRestrictions.invalid_module_members_default.with_child_unrestricted(
      "datetime", "date", "today",
    ),
)

# Example 2: Restrict the datetime.date class from being used
restrictions = dataclasses.replace(
    SandboxRestrictions.default,
    invalid_module_members=SandboxRestrictions.invalid_module_members_default | SandboxMatcher(
      children={"datetime": SandboxMatcher(use={"date"})},
    ),
)

worker = Worker(
    ...,
    workflow_runner=SandboxedWorkflowRunner(restrictions=restrictions),
)

Import Notification Policy

Control warnings/errors for sandbox import issues. Recommended for catching potential problems:

from temporalio import workflow
from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions

restrictions = SandboxRestrictions.default.with_import_notification_policy(
    workflow.SandboxImportNotificationPolicy.WARN_ON_DYNAMIC_IMPORT
    | workflow.SandboxImportNotificationPolicy.WARN_ON_UNINTENTIONAL_PASSTHROUGH
)

worker = Worker(
    ...,
    workflow_runner=SandboxedWorkflowRunner(restrictions=restrictions),
)
  • WARN_ON_DYNAMIC_IMPORT (default) - warns on imports after initial workflow load
  • WARN_ON_UNINTENTIONAL_PASSTHROUGH - warns when modules are imported into sandbox without explicit passthrough (not default, but highly recommended for catching missing passthroughs)
  • RAISE_ON_UNINTENTIONAL_PASSTHROUGH - raise instead of warn

Override per-import with the context manager:

with workflow.unsafe.sandbox_import_notification_policy(
    workflow.SandboxImportNotificationPolicy.SILENT
):
    import pydantic  # No warning for this import

Disable Lazy sys.modules Passthrough

By default, passthrough modules are lazily added to the sandbox's sys.modules when accessed. To require explicit imports:

import dataclasses
from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions

restrictions = dataclasses.replace(
    SandboxRestrictions.default,
    disable_lazy_sys_module_passthrough=True,
)

worker = Worker(
    ...,
    workflow_runner=SandboxedWorkflowRunner(restrictions=restrictions),
)

When True, passthrough modules must be explicitly imported to appear in the sandbox's sys.modules.

File Organization

Critical: Keep workflow definitions in separate files from activity definitions.

The sandbox reloads workflow definition files on every execution. Minimizing file contents improves Worker performance.

my_temporal_app/
├── workflows/
│   └── order.py         # Only workflow classes
├── activities/
│   └── payment.py       # Only activity functions
├── models/
│   └── order.py         # Shared data models
├── worker.py            # Worker setup, imports both
└── starter.py           # Client code

Common Issues

Import Errors

Error: Cannot import 'pydantic' in sandbox

Fix: Use pass-through:

with workflow.unsafe.imports_passed_through():
    import pydantic

Non-Determinism from Libraries

Some libraries do internal caching or use current time:

# May cause non-determinism
import some_library
result = some_library.cached_operation()  # Cache changes between replays

Fix: Move to activity or use pass-through with caution.

Best Practices

  1. Separate workflow and activity files for performance
  2. Use pass-through explicitly for third-party libraries
  3. Keep workflow files small to minimize reload time
  4. Move I/O to activities always
  5. Test with replay to catch sandbox issues early