# Build a durable agent on Amazon Bedrock AgentCore

> For the complete documentation index, see [llms.txt](https://docs.temporal.io/llms.txt).
> Any documentation page is available as raw Markdown by appending `.md` to its URL.

> A Temporal Workflow preserves conversation state while AgentCore Runtime supplies serverless Worker compute for a Strands agent.

> **Pre-release**
> Amazon Bedrock AgentCore Runtime support is in Pre-release, and its APIs may change in backwards-incompatible ways.

This guide builds a data-analysis agent that can continue a conversation after the compute running it has stopped. A
Temporal Workflow coordinates each turn and preserves the state needed to continue. Strands defines how the agent uses
a model and tools. Amazon Bedrock AgentCore Runtime supplies serverless compute for the Temporal Worker, and AgentCore
Code Interpreter supplies an isolated environment for running code.

The result separates the lifetime of the agent from the lifetime of its compute. The Workflow can remain open for days
or months without keeping an AgentCore Runtime active.

## What you will build

The agent has one Workflow Execution for each conversation. The application starts that Workflow and sends prompts to
an `ask` Update handler through Temporal. It does not send prompts to the AgentCore Runtime endpoint. Each Update
returns the agent's answer to the application.

The agent uses Amazon Bedrock for model inference and AgentCore Code Interpreter for calculations. After a turn, its
Worker retires while the Workflow remains open. A later prompt starts new Worker capacity and continues the same
conversation.

## Architecture

The application sends prompts to a Temporal Workflow through an Update. The Workflow identifies the conversation and
records its execution progress. When its Task Queue has work, Serverless Workers starts an AgentCore Runtime session
that hosts a Temporal Worker. A later Task can run on a different Runtime session because Temporal reconstructs the
Workflow from Event History before it continues.

Strands runs the agent loop for each turn. The Temporal Strands integration schedules model calls and tool calls as
Activities. Each call has its own timeout, Retry Policy, and recorded result. After the Worker returns the answer, the
Workflow can wait for another prompt without keeping an AgentCore Runtime session active.

```mermaid
flowchart LR
    APP[Application] -->|Prompts and replies| WF[Temporal Workflow]
    WF --> TQ[Task Queue]
    TQ --> W[Temporal Worker<br/>AgentCore Runtime session A or B]
    W --> AWS[Bedrock and<br/>AgentCore services]
    W --> STORE[External conversation<br/>and artifact storage]
    WF -.->|Durable references| STORE
```

Place state according to how long it must remain available:

| State | Location |
|---|---|
| Execution progress and bounded working context | Temporal Workflow |
| Large or unbounded conversation content, uploads, and artifacts | Durable external storage, with stable references in the Workflow |
| Knowledge shared across conversations, such as user preferences | [AgentCore Memory](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html), accessed from an Activity |
| Temporary caches and Code Interpreter files | AgentCore Runtime or tool session, when the application can tolerate losing them |

For clarity, this sample keeps its complete Strands message list in Workflow state. That works for a bounded
conversation. For a conversation that can grow without a fixed limit, store its raw content externally and carry a
reference or bounded summary in the Workflow. Retrieve external content through an Activity before using it in a model
call. Use [Continue-As-New](/develop/python/integrations/strands-agents#handle-long-running-chat-sessions) to start a new
Event History with only the references and working context that the next execution needs.

## Prerequisites

To build and run the agent locally, you need:

- Python 3.10 or later and [`uv`](https://docs.astral.sh/uv/).
- The [Temporal CLI](/cli/setup-cli) to run a local Temporal development server.
- AWS credentials with access to the Bedrock model selected by Strands and permission to use AgentCore Code
  Interpreter. The sample includes the required Code Interpreter IAM policy.

To deploy the Worker, you also need:

- A Temporal Cloud account with an AWS-hosted Namespace and access to the AgentCore Serverless Workers Pre-release.
- An AWS account in an [AgentCore-supported Region](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-regions.html).
- The AWS and AgentCore tools and permissions listed in the
  [Serverless Worker deployment prerequisites](/production-deployment/worker-deployments/serverless-workers/agentcore#prerequisites).

This guide uses the
[durable AgentCore sample](https://github.com/temporalio/documentation-sdk-code-examples/tree/docs/durable-agent-agentcore-sample/python-agentcore-durable-agent),
which contains the AgentCore project, Runtime handler, IAM policy, and Code Interpreter Activity. Clone the sample
repository and install the application dependencies:

```bash
git clone --branch docs/durable-agent-agentcore-sample --single-branch \
  https://github.com/temporalio/documentation-sdk-code-examples.git
cd documentation-sdk-code-examples/python-agentcore-durable-agent
uv sync
```

## 1. Build the agent locally 

The [durable AgentCore sample](https://github.com/temporalio/documentation-sdk-code-examples/tree/docs/durable-agent-agentcore-sample/python-agentcore-durable-agent)
defines `execute_code` as a Temporal Activity. It uses the Workflow Id as the Code Interpreter session name so two
Workflow Executions handled by the same process do not share a sandbox. The name does not make the sandbox durable
across Worker replacement.

<!--SNIPSTART python-agentcore-durable-agent-code-activity-->
[python-agentcore-durable-agent/activities.py](https://github.com/temporalio/documentation-sdk-code-examples/blob/docs/durable-agent-agentcore-sample/python-agentcore-durable-agent/activities.py)
```py
@activity.defn
def execute_code(
    code: str, language: LanguageType = LanguageType.PYTHON
) -> dict[str, Any]:
    interpreter = AgentCoreCodeInterpreter(
        region=os.environ.get("AWS_REGION", "us-west-2"),
        session_name=activity.info().workflow_id,
    )
    return interpreter.execute_code(
        ExecuteCodeAction(type="executeCode", code=code, language=language)
    )

```
<!--SNIPEND-->

The Activity boundary gives the tool call a separate timeout, Retry Policy, and result in Event History. It also keeps
AWS calls out of deterministic Workflow code.

Define a Workflow that accepts multiple prompts:

<!--SNIPSTART python-agentcore-durable-agent-workflow-->
[python-agentcore-durable-agent/workflows.py](https://github.com/temporalio/documentation-sdk-code-examples/blob/docs/durable-agent-agentcore-sample/python-agentcore-durable-agent/workflows.py)
```py
@workflow.defn
class DurableAgentWorkflow:
    def __init__(self) -> None:
        self._done = False
        self._lock = asyncio.Lock()
        self._agent = TemporalAgent(
            model="bedrock",
            start_to_close_timeout=timedelta(seconds=60),
            system_prompt=SYSTEM_PROMPT,
            tools=[
                activity_as_tool(
                    execute_code,
                    start_to_close_timeout=timedelta(minutes=2),
                )
            ],
        )

    @workflow.update
    async def ask(self, prompt: str) -> str:
        async with self._lock:
            result = await self._agent.invoke_async(prompt)
            return str(result).strip()

    @workflow.signal
    def finish(self) -> None:
        self._done = True

    @workflow.run
    async def run(self) -> None:
        await workflow.wait_condition(lambda: self._done)
        await workflow.wait_condition(workflow.all_handlers_finished)

```
<!--SNIPEND-->

`TemporalAgent` is a Strands `Agent` adapted to run inside a Workflow. It retains the Strands message list between
calls to `invoke_async`. The Temporal Strands plugin runs model calls as Activities, and `activity_as_tool` runs the
Code Interpreter tool as an Activity. Configure retries through Temporal Activity Retry Policies rather than a Strands
retry strategy.

The lock makes the agent process one prompt at a time. The `run` method waits until the `finish` Signal arrives, so the
Workflow remains available between turns. This wait is durable and does not keep a Python process running.

Register `DurableAgentWorkflow`, `execute_code`, and `StrandsPlugin` on a local Worker. The
[sample Worker](https://github.com/temporalio/documentation-sdk-code-examples/blob/docs/durable-agent-agentcore-sample/python-agentcore-durable-agent/local_worker.py)
also creates the executor required by the synchronous `execute_code` Activity:

<!--SNIPSTART python-agentcore-durable-agent-local-worker-->
[python-agentcore-durable-agent/local_worker.py](https://github.com/temporalio/documentation-sdk-code-examples/blob/docs/durable-agent-agentcore-sample/python-agentcore-durable-agent/local_worker.py)
```py
async def main() -> None:
    client = await Client.connect(
        "localhost:7233",
        plugins=[StrandsPlugin()],
    )

    with ThreadPoolExecutor(max_workers=4) as activity_executor:
        worker = Worker(
            client,
            task_queue=TASK_QUEUE,
            workflows=[DurableAgentWorkflow],
            activities=[execute_code],
            activity_executor=activity_executor,
        )
        await worker.run()

```
<!--SNIPEND-->

Start the Temporal development server, then start the Worker in another terminal:

```bash
temporal server start-dev
```

```bash
uv run python local_worker.py
```

The sample's chat client starts a Workflow and sends each prompt as an Update:

<!--SNIPSTART python-agentcore-durable-agent-chat-client-->
[python-agentcore-durable-agent/chat.py](https://github.com/temporalio/documentation-sdk-code-examples/blob/docs/durable-agent-agentcore-sample/python-agentcore-durable-agent/chat.py)
```py
async def main() -> None:
    client = await Client.connect(
        "localhost:7233",
        plugins=[StrandsPlugin()],
    )
    handle = await client.start_workflow(
        DurableAgentWorkflow.run,
        id=f"durable-agent-{uuid.uuid4()}",
        task_queue=TASK_QUEUE,
    )

    while prompt := input("You: "):
        if prompt == "/finish":
            await handle.signal(DurableAgentWorkflow.finish)
            return
        answer = await handle.execute_update(DurableAgentWorkflow.ask, prompt)
        print(f"Agent: {answer}")

```
<!--SNIPEND-->

Run the client in a third terminal:

```bash
uv run python chat.py
```

Ask a question that requires calculation, then ask a follow-up that depends on the first answer. Enter `/finish` to
close the Workflow. In the Temporal Web UI, the Event History shows the `ask` Update, model Activities, and
`execute_code` Activity for each turn.

## 2. Run the Worker on AgentCore Runtime 

Local development uses a continuously running Worker. On AgentCore Runtime, the Worker starts inside the Runtime's HTTP
handler and returns when its idle policy decides to release the compute.

The AgentCore Runtime handler registers the `DurableAgentWorkflow` and `execute_code` definitions from
[Build the agent locally](#build-the-agent-locally). It adds Worker Versioning and the Activity-based idle tracker from
the [Python AgentCore Worker guide](/develop/python/workers/serverless-workers/agentcore#stop-and-drain-the-worker), then
runs the Worker inside the Runtime handler:

<!--SNIPSTART python-agentcore-durable-agent-runtime-handler-->
[python-agentcore-durable-agent/agentcore_worker.py](https://github.com/temporalio/documentation-sdk-code-examples/blob/docs/durable-agent-agentcore-sample/python-agentcore-durable-agent/agentcore_worker.py)
```py
@app.entrypoint
@app.async_task
async def invoke(payload: dict) -> dict:
    client = await Client.connect(
        required_env("TEMPORAL_ADDRESS"),
        namespace=required_env("TEMPORAL_NAMESPACE"),
        api_key=required_env("TEMPORAL_API_KEY"),
        tls=True,
        plugins=[StrandsPlugin()],
    )
    tracker = ActivityTracker()

    with ThreadPoolExecutor(max_workers=4) as activity_executor:
        worker = Worker(
            client,
            task_queue=os.environ.get("TEMPORAL_TASK_QUEUE", TASK_QUEUE),
            workflows=[DurableAgentWorkflow],
            activities=[execute_code],
            activity_executor=activity_executor,
            interceptors=[tracker],
            deployment_config=WorkerDeploymentConfig(
                version=WorkerDeploymentVersion(
                    deployment_name=os.environ.get(
                        "TEMPORAL_DEPLOYMENT_NAME", DEPLOYMENT_NAME
                    ),
                    build_id=os.environ.get("TEMPORAL_BUILD_ID", BUILD_ID),
                ),
                use_worker_versioning=True,
                default_versioning_behavior=VersioningBehavior.PINNED,
            ),
            graceful_shutdown_timeout=DRAIN,
        )
        async with worker:
            await tracker.wait_until_idle(DEBOUNCE)

    return {"message": "Worker drained"}

```
<!--SNIPEND-->

The invocation payload does not contain a user prompt. Temporal invokes the Runtime endpoint to add Worker capacity.
Clients continue to start and message Workflows through the Temporal Client.

The Runtime does not need a copy of the conversation in a local file or global variable. When a new Worker receives a
Workflow Task, Temporal replays the Workflow's Event History and restores the `TemporalAgent` message list before new
model or tool calls run.

## 3. Deploy the Serverless Worker 

Install the AgentCore CLI and generate the CDK project used by the sample's Runtime definition:

```bash
npm install -g @aws/agentcore
./bootstrap-agentcore-project.sh
```

Follow [Deploy a Serverless Worker on Amazon Bedrock AgentCore Runtime](/production-deployment/worker-deployments/serverless-workers/agentcore)
to deploy the existing AgentCore project and configure its Worker Deployment Version.

For this application, use the same values in each place:

| Setting | Tutorial value |
|---|---|
| Runtime entrypoint | `agentcore_worker.py` |
| Task Queue | `durable-agent` |
| Worker Deployment name | `durable-agent-agentcore` |
| Build ID | A version for this code, such as `1.0.0` |

The AgentCore Runtime execution role needs permission to invoke Bedrock and Code Interpreter. The separate role that
Temporal Cloud assumes needs permission to invoke the AgentCore Runtime endpoint. The deployment guide creates and
configures the second role.

## 4. Talk to the deployed agent 

Start one conversation Workflow. This command returns immediately while the Workflow remains open:

```bash
temporal workflow start \
  --workflow-id durable-agent-alice \
  --type DurableAgentWorkflow \
  --task-queue durable-agent
```

Send the first prompt as an Update and wait for the reply:

```bash
temporal workflow update execute \
  --workflow-id durable-agent-alice \
  --name ask \
  --input '"A film festival has 7 screens with 4 showings per screen. How many screenings can it schedule?"'
```

Temporal starts AgentCore Worker capacity because the Task Queue has work. After the turn completes and the idle period
expires, the Runtime handler drains the Worker and returns. Confirm this in the AgentCore logs:

```bash
agentcore logs --runtime <RUNTIME_NAME>
```

After the Worker has retired, send a follow-up that depends on the first turn:

```bash
temporal workflow update execute \
  --workflow-id durable-agent-alice \
  --name ask \
  --input '"If we add two screenings to the total you calculated, what is the new total?"'
```

Temporal starts capacity again. The new Worker reconstructs the existing Workflow and its Strands messages, so the
agent can interpret "the total you calculated" without depending on the previous Worker process.

End the conversation when it no longer needs to accept prompts:

```bash
temporal workflow signal \
  --workflow-id durable-agent-alice \
  --name finish
```

## 5. Test recovery 

Worker retirement between turns tests one form of recovery. You can also interrupt compute while a model or tool
Activity is running. Start a prompt that takes long enough to observe, find the active Runtime session identifier in the
AgentCore logs, and stop that session:

```bash
aws bedrock-agentcore stop-runtime-session \
  --agent-runtime-arn <AGENT_RUNTIME_ARN> \
  --runtime-session-id <RUNTIME_SESSION_ID> \
  --region <AWS_REGION>
```

For the required IAM permission and API behavior, see
[Stop a running session](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-stop-session.html).

The Activity attempt running on that Worker is interrupted. Temporal keeps the Workflow state and schedules the
Activity again according to its Retry Policy. Serverless Workers starts new AgentCore capacity to process the Task. In
the Temporal Web UI, inspect the Activity attempts and confirm that the Workflow continues without restarting the
conversation.

An Activity can run more than once if its Worker stops after making an external change but before reporting completion.
Use an idempotency key for tools that change external state. The Workflow Id plus a stable operation identifier is a
common choice. Code execution used only to calculate an answer does not make an external business change, so it is a
safe recovery demonstration.
