mirror of
https://github.com/temporalio/skill-temporal-developer.git
synced 2026-09-14 13:52:58 +08:00
b5719bc143
* 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>
234 lines
6.8 KiB
Markdown
234 lines
6.8 KiB
Markdown
# 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
|