{"slug": "build-durable-agents-with-pydantic-ai-and-temporal", "title": "Build Durable Agents With Pydantic AI And Temporal", "summary": "Pydantic AI has introduced a native integration with Temporal, a durable execution engine, enabling agentic applications to resume from failures rather than restarting. The TemporalAgent class wraps existing agents, converting tool calls and model requests into Temporal Activities that can be retried on failure. This integration allows workflows to be replayed from event history, ensuring progress is preserved across crashes.", "body_md": "Agentic applications can fail mid-run: a network call drops, a server process crashes, an LLM request times out. Without durability built in, these failures mean lost progress and repeated (wasted) LLM calls when the process has to start over.\n\nPydantic AI provides a native integration with **Temporal**, a durable execution engine. Temporal records each step of a workflow's execution as an event history, so if a process crashes, it can resume from where it left off instead of starting over.\n\nPydantic AI exposes this through the `TemporalAgent`\n\nclass, which wraps an existing agent and converts its tool calls, MCP calls, and model requests into Temporal Activities.\n\n``` python\nfrom pydantic_ai import Agent\nfrom pydantic_ai.durable_exec.temporal import TemporalAgent\n\nplanning_agent = Agent(\n    model='anthropic:claude-fable-5',\n)\ntemporal_agent = TemporalAgent(\n    wrapped=planning_agent\n)\n```\n\nYou call `.run(...)`\n\non `temporal_agent`\n\nthe same way you would on the underlying agent. The difference is under the hood: each tool call, MCP call, or model request now runs as a Temporal Activity.\n\n**Activity** — a function that performs a unit of work: an LLM call, an API request, a file write. If it fails, Temporal can retry it based on a configured retry policy.\n\n``` python\nfrom temporalio import activity\n\n@activity.defn\nasync def create_sandbox(user_id: str) -> str:\n    sandbox = await AsyncSandbox.create(template=\"node-expo-builder\")\n    return sandbox.sandbox_id\n```\n\n**Workflow** — orchestrates Activities, defining what runs and in what order. Workflow code must be deterministic (no direct network calls, no random values, no reading the system clock), since Temporal replays it to reconstruct state.\n\n``` python\nfrom temporalio import workflow\nfrom datetime import timedelta\n\n@workflow.defn\nclass ExampleWorkflow:\n    @workflow.run\n    async def run(self, prompt: str) -> str:\n        sandbox_id = await workflow.execute_activity(\n            create_sandbox,\n            args=[self.user_id],\n            start_to_close_timeout=timedelta(minutes=2),\n        )\n        return sandbox_id\n```\n\n**Worker** — polls a task queue for scheduled work and executes it. A Workflow or Activity must be registered with a Worker before it can run.\n\n``` python\nimport asyncio\nfrom temporalio.client import Client\nfrom temporalio.worker import Worker\n\nasync def main():\n    client = await Client.connect(\"localhost:7233\")\n\n    worker = Worker(\n        client,\n        task_queue=\"coding-agent\",\n        workflows=[ExampleWorkflow],\n        activities=[create_sandbox],\n    )\n    await worker.run()\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nThe task queue name is the only link between the code that starts a Workflow and the Worker that executes it — they never call each other directly.\n\nWorkers run Activities and Workflows differently: Activities run their function body when scheduled, and may be retried on failure per their retry policy. Workflows are replayed against their event history each time they need to advance, which is what lets a Workflow resume correctly on a new Worker after a crash.\n\nOnce a Workflow is running, Signal and Query let you interact with it from outside.\n\n**Signal** sends data into a running Workflow, asynchronously, with no return value:\n\n``` php\n@workflow.signal\nasync def approve_plan(self) -> None:\n    self._plan_approved = True\n\n@workflow.signal\nasync def reject_plan(self, feedback: str = \"\") -> None:\n    self._plan_approved = False\n    self._plan_feedback = feedback\n```\n\nSignals only work while a Workflow is still running — once it's completed, failed, or been terminated, it can no longer receive one.\n\n**Query** reads a Workflow's current state, synchronously and without side effects:\n\n``` php\n@workflow.query\ndef state(self) -> WorkflowState:\n    return WorkflowState(\n        status=self._status,\n        project_name=self._project_name,\n        preview_url=self._preview_url,\n    )\n```\n\nQueries can also run against a **completed** Workflow, within your namespace's retention period. This doesn't extend to terminated Workflows, which aren't safely queryable.\n\nBecause Temporal keeps a full event history, a Workflow can pause and resume without losing state — a pattern suited to human approval steps.\n\n``` python\nfrom temporalio import workflow\n\n@workflow.defn\nclass ExampleWorkflow:\n    @workflow.run\n    async def run(self):\n        while True:\n            self._plan_approved = None\n            await workflow.wait_condition(lambda: self._plan_approved is not None)\n\n            if self._plan_approved:\n                break\n```\n\n`workflow.wait_condition`\n\npauses execution until the given condition is true. Here, the Workflow waits until a Signal (like `approve_plan`\n\nabove) sets `self._plan_approved`\n\n. If the process running this Workflow crashes while it's paused, a new Worker can pick it up and it resumes waiting at the same point.\n\nThis example has no timeout, so it waits indefinitely. In production, you'd typically pass a `timeout`\n\nto `wait_condition`\n\nand handle the case where no decision ever comes.\n\nThis is part of an ongoing series on my [channel](https://youtube.com/@joxiahdev), **Mastering Pydantic AI**.", "url": "https://wpnews.pro/news/build-durable-agents-with-pydantic-ai-and-temporal", "canonical_source": "https://dev.to/joxiahdev/build-durable-agents-with-pydantic-ai-and-temporal-3e67", "published_at": "2026-07-18 11:00:00+00:00", "updated_at": "2026-07-18 11:28:42.403223+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Pydantic AI", "Temporal"], "alternates": {"html": "https://wpnews.pro/news/build-durable-agents-with-pydantic-ai-and-temporal", "markdown": "https://wpnews.pro/news/build-durable-agents-with-pydantic-ai-and-temporal.md", "text": "https://wpnews.pro/news/build-durable-agents-with-pydantic-ai-and-temporal.txt", "jsonld": "https://wpnews.pro/news/build-durable-agents-with-pydantic-ai-and-temporal.jsonld"}}