Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 24 additions & 54 deletions docs/develop/python/integrations/strands-agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ Create a Workflow that holds a `TemporalAgent` and invokes it with a prompt. The
maximum time each model call Activity can run:

<!--SNIPSTART python-strands-hello-world-workflow-->

[strands_plugin/hello_world/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hello_world/workflow.py)

```py
from datetime import timedelta

Expand All @@ -92,8 +90,9 @@ class HelloWorldWorkflow:
async def run(self, prompt: str) -> str:
result = await self.agent.invoke_async(prompt)
return str(result)
```


```
<!--SNIPEND-->

:::caution
Expand All @@ -109,9 +108,7 @@ Create a Worker that registers the Workflow and the `StrandsPlugin`. The plugin
that handle model calls:

<!--SNIPSTART python-strands-hello-world-worker-->

[strands_plugin/hello_world/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hello_world/run_worker.py)

```py
import asyncio
import os
Expand Down Expand Up @@ -142,7 +139,6 @@ async def main() -> None:
if __name__ == "__main__":
asyncio.run(main())
```

<!--SNIPEND-->

**3. Run the Workflow**
Expand All @@ -151,9 +147,7 @@ Start the Workflow from a separate client script. This example sends the prompt
and prints the agent's response:

<!--SNIPSTART python-strands-hello-world-run-workflow-->

[strands_plugin/hello_world/run_workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hello_world/run_workflow.py)

```py
import asyncio
import os
Expand All @@ -179,7 +173,6 @@ async def main() -> None:
if __name__ == "__main__":
asyncio.run(main())
```

<!--SNIPEND-->

## Build the agent
Expand Down Expand Up @@ -233,9 +226,7 @@ on the Worker, and pass them to the agent using `activity_as_tool`.
Define an Activity for the tool:

<!--SNIPSTART python-strands-tools-activity-->

[strands_plugin/tools/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/tools/workflow.py)

```py
@activity.defn
async def fetch_weather(city: str) -> dict:
Expand All @@ -245,16 +236,15 @@ async def fetch_weather(city: str) -> dict:
"temperature_f": 72,
"conditions": "sunny",
}
```


```
<!--SNIPEND-->

Pass the Activity to the agent in the Workflow using `activity_as_tool`:

<!--SNIPSTART python-strands-tools-workflow-->

[strands_plugin/tools/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/tools/workflow.py)

```py
@workflow.defn
class ToolsWorkflow:
Expand All @@ -278,16 +268,15 @@ class ToolsWorkflow:
async def run(self, prompt: str) -> str:
result = await self.agent.invoke_async(prompt)
return str(result)
```


```
<!--SNIPEND-->

Register the Activity functions on the Worker:

<!--SNIPSTART python-strands-tools-worker-->

[strands_plugin/tools/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/tools/run_worker.py)

```py
import asyncio
import os
Expand Down Expand Up @@ -323,7 +312,6 @@ async def main() -> None:
if __name__ == "__main__":
asyncio.run(main())
```

<!--SNIPEND-->

If you are using built-in `strands_tools`, wrap them in a thin async function decorated with `@activity.defn` so they
Expand All @@ -343,22 +331,19 @@ Temporal Activity. The following example shows both patterns in one `HookProvide
Workflow context (deterministic), while `persist_tool_call` runs as an Activity (I/O-safe):

<!--SNIPSTART python-strands-hooks-activity-->

[strands_plugin/hooks/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hooks/workflow.py)

```py
@activity.defn
async def persist_tool_call(tool_name: str) -> None:
# In production, write to a database / S3 / your audit pipeline.
activity.logger.info(f"audit: tool {tool_name} completed")
```


```
<!--SNIPEND-->

<!--SNIPSTART python-strands-hooks-provider-->

[strands_plugin/hooks/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hooks/workflow.py)

```py
class AuditHook(HookProvider):
def __init__(self) -> None:
Expand All @@ -377,8 +362,9 @@ class AuditHook(HookProvider):

def _record(self, event: AfterToolCallEvent) -> None:
self.fired.append(event.tool_use["name"])
```


```
<!--SNIPEND-->

:::caution
Expand All @@ -405,9 +391,7 @@ plugin registers a per-server Activity and connects at Worker startup to enumera
Define the Workflow with a `TemporalMCPClient`:

<!--SNIPSTART python-strands-mcp-workflow-->

[strands_plugin/mcp/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/mcp/workflow.py)

```py
from datetime import timedelta

Expand All @@ -431,16 +415,15 @@ class MCPWorkflow:
async def run(self, prompt: str) -> str:
result = await self.agent.invoke_async(prompt)
return str(result)
```


```
<!--SNIPEND-->

Register the MCP client factory on the Worker:

<!--SNIPSTART python-strands-mcp-worker {"selectedLines": ["6-10", "17-25", "28-41"]}-->

[strands_plugin/mcp/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/mcp/run_worker.py)

```py
# ...
from mcp import StdioServerParameters, stdio_client
Expand Down Expand Up @@ -474,7 +457,6 @@ async def main() -> None:
print("Worker started. Ctrl+C to exit.")
await worker.run()
```

<!--SNIPEND-->

Each factory returns a fully configured `MCPClient`, so you can pass options like `tool_filters`, `prefix`,
Expand Down Expand Up @@ -509,9 +491,7 @@ A hook on an interruptible event such as `BeforeToolCallEvent` can pause the age
Define the approval hook:

<!--SNIPSTART python-strands-human-in-the-loop-hook-->

[strands_plugin/human_in_the_loop/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/human_in_the_loop/workflow.py)

```py
class ApprovalHook(HookProvider):
def register_hooks(self, registry: HookRegistry, **kwargs: object) -> None:
Expand All @@ -526,16 +506,15 @@ class ApprovalHook(HookProvider):
)
if approval != "approve":
event.cancel_tool = "denied"
```


```
<!--SNIPEND-->

The Workflow waits for a Signal carrying the approval response, then resumes the agent:

<!--SNIPSTART python-strands-human-in-the-loop-workflow-->

[strands_plugin/human_in_the_loop/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/human_in_the_loop/workflow.py)

```py
@workflow.defn
class HumanInTheLoopWorkflow:
Expand Down Expand Up @@ -572,8 +551,9 @@ class HumanInTheLoopWorkflow:
]
result = await self.agent.invoke_async(responses)
return str(result)
```


```
<!--SNIPEND-->

#### Interrupt from a tool
Expand All @@ -599,9 +579,7 @@ The same approach works from an `activity_as_tool`-wrapped Activity. The plugin'
Define the Activity that raises the interrupt:

<!--SNIPSTART python-strands-activity-interrupt-activity-->

[strands_plugin/activity_interrupt/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/activity_interrupt/workflow.py)

```py
@activity.defn
async def delete_thing(name: str) -> str:
Expand All @@ -615,8 +593,9 @@ async def delete_thing(name: str) -> str:
)
)
return f"deleted {name}"
```


```
<!--SNIPEND-->

:::caution
Expand All @@ -629,9 +608,7 @@ Attach `StrandsPlugin` to the **client** (not just the Worker) for Activity-tool
Workers built from that client pick up the plugin automatically:

<!--SNIPSTART python-strands-activity-interrupt-worker-->

[strands_plugin/activity_interrupt/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/activity_interrupt/run_worker.py)

```py
import asyncio
import os
Expand Down Expand Up @@ -667,7 +644,6 @@ async def main() -> None:
if __name__ == "__main__":
asyncio.run(main())
```

<!--SNIPEND-->

### Return structured data from an agent
Expand All @@ -677,9 +653,7 @@ The plugin defaults to the [`pydantic_data_converter`](/develop/python/data-hand
serialize cleanly across the Activity and Workflow boundary:

<!--SNIPSTART python-strands-structured-output-workflow-->

[strands_plugin/structured_output/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/structured_output/workflow.py)

```py
from datetime import timedelta

Expand Down Expand Up @@ -707,8 +681,9 @@ class StructuredOutputWorkflow:
result = await self.agent.invoke_async(prompt)
assert isinstance(result.structured_output, PersonInfo)
return result.structured_output
```


```
<!--SNIPEND-->

### Stream agent output to clients
Expand All @@ -723,9 +698,7 @@ published from inside the model Activity. Subscribers read events through `Workf
Define the Workflow with a `WorkflowStream` and a streaming topic:

<!--SNIPSTART python-strands-streaming-workflow-->

[strands_plugin/streaming/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/streaming/workflow.py)

```py
from datetime import timedelta

Expand All @@ -747,16 +720,15 @@ class StreamingWorkflow:
async def run(self, prompt: str) -> str:
result = await self.agent.invoke_async(prompt)
return str(result)
```


```
<!--SNIPEND-->

Subscribe to the stream from a client:

<!--SNIPSTART python-strands-streaming-client-->

[strands_plugin/streaming/run_workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/streaming/run_workflow.py)

```py
import asyncio
import os
Expand Down Expand Up @@ -806,7 +778,6 @@ async def main() -> None:
if __name__ == "__main__":
asyncio.run(main())
```

<!--SNIPEND-->

## Run in production
Expand Down Expand Up @@ -845,9 +816,7 @@ the chat ends or Temporal suggests continue-as-new. When it does, the Workflow d
fresh execution with the agent's accumulated messages:

<!--SNIPSTART python-strands-continue-as-new-workflow-->

[strands_plugin/continue_as_new/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/continue_as_new/workflow.py)

```py
import asyncio
from dataclasses import dataclass, field
Expand Down Expand Up @@ -901,8 +870,9 @@ class ChatWorkflow:

if not self._done:
workflow.continue_as_new(ChatInput(messages=self._agent.messages))
```


```
<!--SNIPEND-->

### Add tracing with OpenTelemetry
Expand Down