* 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>
6.2 KiB
Python SDK Data Handling
Overview
The Python SDK uses data converters to serialize/deserialize workflow inputs, outputs, and activity parameters.
Default Data Converter
The default converter handles:
Nonebytes(as binary)- Protobuf messages
- JSON-serializable types (dict, list, str, int, float, bool)
Pydantic Integration
Use Pydantic models for validated, typed data.
In your workflow definition, just use input and result types that subclass pydantic.BaseModel:
from pydantic import BaseModel
class OrderInput(BaseModel):
order_id: str
items: list[str]
total: float
customer_email: str
class OrderResult(BaseModel):
order_id: str
status: str
tracking_number: str | None = None
@workflow.defn
class OrderWorkflow:
@workflow.run
async def run(self, input: OrderInput) -> OrderResult:
# Pydantic validation happens automatically
return OrderResult(
order_id=input.order_id,
status="completed",
tracking_number="TRK123",
)
And when you configure the client, pass the pydantic_data_converter:
from temporalio.contrib.pydantic import pydantic_data_converter
# Configure client with Pydantic support
client = await Client.connect(
"localhost:7233",
namespace="default",
data_converter=pydantic_data_converter,
)
Custom Data Conversion
Usually the easiest way to do this is via implementing an EncodingPayloadConverter and CompositePayloadConverter. See:
- https://raw.githubusercontent.com/temporalio/samples-python/refs/heads/main/custom_converter/shared.py
- https://raw.githubusercontent.com/temporalio/samples-python/refs/heads/main/custom_converter/starter.py
for an extended example.
Payload Encryption
Encrypt sensitive workflow data.
from temporalio.converter import PayloadCodec
from temporalio.api.common.v1 import Payload
from cryptography.fernet import Fernet
from typing import Sequence
class EncryptionCodec(PayloadCodec):
def __init__(self, key: bytes):
self._fernet = Fernet(key)
async def encode(self, payloads: Sequence[Payload]) -> list[Payload]:
return [
Payload(
metadata={"encoding": b"binary/encrypted"},
# Since encryption uses C extensions that give up the GIL, we can avoid blocking the async event loop here.
data=await asyncio.to_thread(self._fernet.encrypt, p.SerializeToString()),
)
for p in payloads
]
async def decode(self, payloads: Sequence[Payload]) -> list[Payload]:
result = []
for p in payloads:
if p.metadata.get("encoding") == b"binary/encrypted":
decrypted = await asyncio.to_thread(self._fernet.decrypt, p.data)
decoded = Payload()
decoded.ParseFromString(decrypted)
result.append(decoded)
else:
result.append(p)
return result
# Apply encryption codec
client = await Client.connect(
"localhost:7233",
namespace="default",
data_converter=DataConverter(
payload_codec=EncryptionCodec(encryption_key),
),
)
Search Attributes
Custom searchable fields for workflow visibility. These can be created at workflow start:
from temporalio.common import (
SearchAttributeKey,
SearchAttributePair,
TypedSearchAttributes,
)
from datetime import datetime
from datetime import timezone
ORDER_ID = SearchAttributeKey.for_keyword("OrderId")
ORDER_STATUS = SearchAttributeKey.for_keyword("OrderStatus")
ORDER_TOTAL = SearchAttributeKey.for_float("OrderTotal")
CREATED_AT = SearchAttributeKey.for_datetime("CreatedAt")
# At workflow start
handle = await client.start_workflow(
OrderWorkflow.run,
order,
id=f"order-{order.id}",
task_queue="orders",
search_attributes=TypedSearchAttributes([
SearchAttributePair(ORDER_ID, order.id),
SearchAttributePair(ORDER_STATUS, "pending"),
SearchAttributePair(ORDER_TOTAL, order.total),
SearchAttributePair(CREATED_AT, datetime.now(timezone.utc)),
]),
)
Or upserted during workflow execution:
from temporalio import workflow
from temporalio.common import SearchAttributeKey, SearchAttributePair, TypedSearchAttributes
ORDER_STATUS = SearchAttributeKey.for_keyword("OrderStatus")
@workflow.defn
class OrderWorkflow:
@workflow.run
async def run(self, order: Order) -> str:
# ... process order ...
# Update search attribute
workflow.upsert_search_attributes(TypedSearchAttributes([
SearchAttributePair(ORDER_STATUS, "completed"),
]))
return "done"
Querying Workflows by Search Attributes
# List workflows using search attributes
async for workflow in client.list_workflows(
'OrderStatus = "processing" OR OrderStatus = "pending"'
):
print(f"Workflow {workflow.id} is still processing")
Workflow Memo
Store arbitrary metadata with workflows (not searchable).
# Set memo at workflow start
await client.execute_workflow(
OrderWorkflow.run,
order,
id=f"order-{order.id}",
task_queue="orders",
memo={
"customer_name": order.customer_name,
"notes": "Priority customer",
},
)
# Read memo from workflow
@workflow.defn
class OrderWorkflow:
@workflow.run
async def run(self, order: Order) -> str:
notes: str = workflow.memo_value("notes", type_hint=str)
...
Deterministic APIs for Values
Use these APIs within workflows for deterministic random values and UUIDs:
@workflow.defn
class MyWorkflow:
@workflow.run
async def run(self) -> str:
# Deterministic UUID (same on replay)
unique_id = workflow.uuid4()
# Deterministic random (same on replay)
rng = workflow.random()
value = rng.randint(1, 100)
return str(unique_id)
Best Practices
- Use Pydantic for input/output validation
- Keep payloads small—see
references/core/gotchas.mdfor limits - Encrypt sensitive data with PayloadCodec
- Use dataclasses for simple data structures
- Use
workflow.uuid4()andworkflow.random()for deterministic values