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

234 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```python
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:
```python
# 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
```python
@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.
```python
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:
```python
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:
```python
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:
```python
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:
```python
with workflow.unsafe.imports_passed_through():
import pydantic
```
### Non-Determinism from Libraries
Some libraries do internal caching or use current time:
```python
# 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