Temporal Python SDK

repository·main·Indexed 22 days ago

https://github.com/temporalio/sdk-python

A framework for authoring durable, scalable, and resilient workflows and activities using Python. The SDK allows developers to execute fault-tolerant, long-running business logic. Key features include a workflow sandbox for determinism, support for signals, queries, and updates, and configurable data converters including Pydantic v2 support. It also provides experimental external storage drivers (such as S3) to offload large payloads and avoid inline payload size limits.

Tokens
53.2K
Snippets
117
Records
176
Agent score
78%

What's inside temporalio

  1. Integrate Google ADK Agents with Temporal

    main

    This integration allows Google ADK Agents to run reliably within Temporal Workflows. It ensures workflow determinism by routing non-deterministic operations (like network I/O and model calls) through Temporal Activities.

    Key benefits include:

    • Automatic Recovery: Handles crashes and stalls using Temporal's native retry and persistence mechanisms.
    • Deterministic Runtime: Automatically replaces non-deterministic calls like time.time() with workflow.now() and uuid.uuid4() with workflow.uuid4() when using the GoogleAdkPlugin.
    • Long-running Tools: Supports long-running tools via Temporal Activities instead of requiring separate microservices.
    • Observability: Provides OpenTelemetry integration for tracing ADK components within the Temporal execution context.
  2. Run simple tools directly inside Temporal Workflows

    main

    For simple, deterministic computations that do not require external I/O, you can define tools directly within the workflow using the standard OpenAI Agents SDK @functiontool annotation.

    Important Restrictions:

    • Tools running directly in the workflow must respect workflow execution restrictions (no I/O or non-deterministic operations).
    • Tools running in the workflow can update OpenAI Agents context (which is read-only for tools run as Temporal activities).
    • If a tool needs to perform I/O, it should be implemented as a Temporal Activity and converted via activity_as_tool instead.
    from temporalio import workflow
    from agents import Agent, Runner, function_tool
    
    @function_tool
    def calculate_circle_area(radius: float) -> float:
        """Calculate the area of a circle given its radius."""
        import math
        return math.pi * radius ** 2
    
    @workflow.defn
    class MathAssistantAgent:
        @workflow.run
        async def run(self, message: str) -> str:
            agent = Agent(
                name="Math Assistant",
                instructions="Use the available tools to help with calculations.",
                tools=[calculate_circle_area],
            )
            result = await Runner.run(agent, input=message)
            return result.final_output
  3. How Signal and Update handlers work

    main

    Signal and update handlers are defined using decorated methods. Client code interacts with them using workflow_handle.signal, workflow_handle.execute_update, or workflow_handle.start_update.

    Key Characteristics

    • Concurrency: Handlers execute as asyncio.Task objects and run concurrently with each other and the main workflow task. If you need to manage shared state between handlers, use asyncio.Lock or asyncio.Semaphore.
    • Lifecycle Management: A workflow may finish (via completion, cancellation, continue_as_new, or failure) while handlers are still running. To prevent warnings and ensure clean shutdowns, you should wait for all handlers to finish using the workflow.all_handlers_finished condition.
    # Ensure all signal/update handlers have finished before the workflow completes
    await workflow.wait_condition(workflow.all_handlers_finished)
  4. Configure S3 driver limits and behavior

    main

    When using S3StorageDriver, keep the following configuration behaviors in mind:

    • Payload Size Threshold: Only payloads at or above ExternalStorage.payload_size_threshold (default: 256 KiB) are offloaded to S3. Set this to 0 to offload every payload.
    • Max Payload Size: S3StorageDriver.max_payload_size (default: 50 MiB) is a hard limit on the serialized size of a single payload. A ValueError is raised if a payload exceeds this. Increase this value if your workflows produce larger payloads.
    • Driver Name: Override S3StorageDriver.driver_name only if you are registering multiple S3StorageDriver instances with different configurations in the same ExternalStorage.drivers list.
    • Bucket Existence: The target S3 bucket must already exist; the driver will not create it.
    • Symmetry: Any driver used to store payloads must also be configured on the component that retrieves them. For example, if a Client uses the driver to store inputs, the Worker must also include that driver in its ExternalStorage.drivers list to read them.
  5. Use Signals, Updates, and Queries in Workflows

    main

    Workflows can interact with external entities via signals, updates, and queries:

    • Signals (@workflow.signal): One-way messages sent to a workflow. They can be async or non-async. They can mutate state or start activities. Use dynamic=True to catch all unhandled signals.
    • Updates (@workflow.update): Similar to signals but allow for input and a return value. You can define a validator using @update_handler_method.validator to reject updates before they are written to history. Validators must be synchronous and cannot mutate state.
    • Queries (@workflow.query): Used to retrieve the current state of a workflow. Queries must not be async and must not mutate any state or call mutating APIs.
  6. Use Nexus for synchronous RPC from workflows

    main

    Nexus is a synchronous RPC protocol that allows Temporal to call operations. Temporal supports calling Nexus operations exclusively from within a workflow.

    There are two types of supported Nexus operations:

    1. @temporalio.nexus.workflow_run_operation: Backed by a Temporal workflow. The handler starts a workflow and returns a token. When the workflow completes, the result is automatically delivered to the caller.
    2. @nexusrpc.handler.sync_operation: A synchronous operation (can be def or async def) that must respond within 10 seconds.

    To use Nexus, you must:

    1. Create namespaces and a Nexus endpoint via the Temporal CLI.
    2. Define a service contract using @nexusrpc.service.
    3. Implement handlers using @service_handler and the appropriate operation decorator.
    4. Register the handler with a Temporal Worker using the nexus_service_handlers argument.
    5. Call operations from a workflow using workflow.create_nexus_client.
    # 1. Define Service Contract
    @nexusrpc.service
    class MyNexusService:
        my_sync_operation: nexusrpc.Operation[MyInput, MyOutput]
        my_workflow_run_operation: nexusrpc.Operation[MyInput, MyOutput]
    
    # 2. Implement Handlers
    @service_handler(service=MyNexusService)
    class MyNexusServiceHandler:
        @sync_operation
        async def my_sync_operation(self, ctx: StartOperationContext, input: MyInput) -> MyOutput:
            return MyOutput(message=f"Hello {input.name}!")
    
        @workflow_run_operation
        async def my_workflow_run_operation(self, ctx: WorkflowRunOperationContext, input: MyInput) -> nexus.WorkflowHandle[MyOutput]:
            return await ctx.start_workflow(WorkflowStartedByNexusOperation.run, input, id=str(uuid.uuid4()))
    
    # 3. Register with Worker
    client = await Client.connect("localhost:7233", namespace="my-handler-namespace")
    worker = Worker(
        client,
        task_queue="my-handler-task-queue",
        workflows=[WorkflowStartedByNexusOperation],
        nexus_service_handlers=[MyNexusServiceHandler()],
    )
    await worker.run()
    
    # 4. Call from Workflow
    @workflow.defn
    class CallerWorkflow:
        def __init__(self):
            self.nexus_client = workflow.create_nexus_client(
                service=MyNexusService, endpoint="my-nexus-endpoint"
            )
    
        @workflow.run
        async def run(self, name: str) -> tuple[MyOutput, MyOutput]:
            # Execute and wait for result
            wf_result = await self.nexus_client.execute_operation(
                MyNexusService.my_workflow_run_operation,
                MyInput(name),
            )
            return wf_result
  7. Use Model Context Protocol (MCP) with OpenAI Agents

    main

    The integration supports Model Context Protocol (MCP) servers via two wrapper types. Because MCP servers operate independently of Temporal, their durability is not automatically managed by Temporal workflows. You must choose the wrapper that matches your server's design:

    1. Stateless MCP Servers: Treat each operation independently (e.g., a weather lookup). These are safe to restart or reconnect. Use StatelessMCPServerProvider to register them with the OpenAIAgentsPlugin in the Worker. In the workflow, access them using openai_agents.workflow.stateless_mcp_server("SERVER_NAME").

    2. Stateful MCP Servers: Maintain session state between calls (e.g., a server requiring a set_location call before get_weather). If the connection fails, Temporal raises an ApplicationError. Because the server state is lost, you must implement your own application-level retry logic to handle these failures.

    Security Warning: When using stateless_mcp_server() or stateful_mcp_server(), you can pass an optional factory_argument. Do not pass secrets, credentials, or API keys through factory_argument, as it is recorded in the Temporal workflow history and may be visible in the Web UI. Resolve credentials inside the server factory instead.

    # Worker Configuration for Stateless MCP
    from temporalio.contrib.openai_agents import ( 
        ModelActivityParameters, 
        OpenAIAgentsPlugin, 
        StatelessMCPServerProvider
    )
    
    filesystem_server = StatelessMCPServerProvider(
        lambda: MCPServerStdio(
            name="FileSystemServer",
            params={
                "command": "npx",
                "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"],
            },
        )
    )
    
    client = await Client.connect(
        "localhost:7233",
        plugins=[
            OpenAIAgentsPlugin(
                model_params=ModelActivityParameters(start_to_close_timeout=timedelta(seconds=60)),
                mcp_server_providers=[filesystem_server],
            ),
        ],
    )
    
    # Workflow Implementation
    @workflow.defn
    class FileSystemWorkflow:
        @workflow.run
        async def run(self, query: str) -> str:
            server = openai_agents.workflow.stateless_mcp_server("FileSystemServer")
            agent = Agent(
                name="File Assistant",
                instructions="Use the filesystem tools to read files.",
                mcp_servers=[server],
            )
            result = await Runner.run(agent, input=query)
            return result.final_output
  8. Note on LangGraph Stores

    main

    LangGraph Store objects (e.g., InMemoryStore) are not supported inside Activity-wrapped nodes.

    Because the Store holds live state that cannot cross the Activity boundary, and Activities may run on different workers than the Workflow, runtime.store will be None inside nodes.

    Alternatives:

    • Use Workflow state for per-run memory.
    • Use an external database (Postgres/Redis) configured on each worker for shared memory across runs.
  9. Configure custom models in StrandsPlugin

    main

    The StrandsPlugin allows you to map model names to factory functions. Each factory is called lazily on the worker (outside the workflow sandbox) and the resulting model is cached.

    In your workflow, you select a model by passing its name to TemporalAgent(model="name", ...).

    If models is omitted from StrandsPlugin, it defaults to a single factory named "bedrock" using BedrockModel().

    from strands.models.anthropic import AnthropicModel
    from strands.models.bedrock import BedrockModel
    
    # workflow
    @workflow.defn
    class MultiModelWorkflow:
        def __init__(self) -> None:
            self.agent_a = TemporalAgent(
                model="claude",
                start_to_close_timeout=timedelta(seconds=60),
            )
            self.agent_b = TemporalAgent(
                model="bedrock",
                start_to_close_timeout=timedelta(seconds=60),
            )
    
    # worker
    Worker(..., plugins=[StrandsPlugin(models={
        "claude": lambda: AnthropicModel(client_args={"api_key": "..."}),
        "bedrock": lambda: BedrockModel(),
    })])
  10. Select storage drivers in ExternalStorage

    main

    When using multiple storage backends, you must provide a driver_selector to determine which driver stores a new payload. Any driver listed in ExternalStorage.drivers that is not selected for a specific payload is still available for retrieval, which facilitates migrating between backends.

    Rules for driver_selector:

    • A selector is required if more than one driver is registered.
    • If the selector returns None, the payload is stored inline in the workflow history instead of being offloaded.
    • The returned driver instance must be one of the instances registered in ExternalStorage.drivers.
    from temporalio.converter import ExternalStorage
    
    options = ExternalStorage(
        drivers=[hot_driver, cold_driver],
        driver_selector=lambda context, payload: (
            hot_driver if payload.ByteSize() < 5 * 1024 * 1024 else cold_driver
        ),
    )
  11. Configure SDK metrics and telemetry

    main

    The SDK emits various metrics by default.

    Global Tags

    To add custom attributes to all emitted metrics, pass global_tags when creating a TelemetryConfig.

    Custom Metrics

    You can emit custom metrics using a metric meter available in different contexts:

    • Workflow code: Use temporalio.workflow.metric_meter
    • Activity code: Use temporalio.activity.metric_meter
    • Application code: Use temporalio.runtime.Runtime.metric_meter

    By default, metrics include namespace, task_queue, and workflow_type/activity_type. Use with_additional_attributes on the meter to include more context.

  12. Handle Worker Shutdown in Activities

    main

    Activities can react to a worker's graceful shutdown using is_worker_shutdown() or the wait_for_worker_shutdown family of functions.

    If the graceful_shutdown_timeout worker parameter is set, the worker will notify activities of the shutdown. If the timeout expires or is not set, the worker will cancel all outstanding activities. Note that shutdown() waits for all activities to complete; if activities do not respect cancellation, the shutdown process may hang.