
Agents Build
- 896 installs
- 2.2k repo stars
- Updated August 4, 2026
- aws/agent-toolkit-for-aws
>.
About
>. Add capabilities to your AgentCore agent project. The agents-build skill documents workflows, constraints, and examples from SKILL.md for agent-assisted execution.
- Add capabilities to your AgentCore agent project.
- Adding cross-session memory to your agent
- Calling your deployed agent from a web app, mobile app, or backend service
- Configuring VPC networking for private resources (RDS, internal APIs)
- Building multi-agent systems with orchestrator/specialist patterns
Agents Build by the numbers
- 896 all-time installs (skills.sh)
- +256 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #202 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
agents-build capabilities & compatibility
- Capabilities
- add capabilities to your agentcore agent project · adding cross session memory to your agent · calling your deployed agent from a web app, mobi · configuring vpc networking for private resources
- Use cases
- documentation
What agents-build says it does
>
npx skills add https://github.com/aws/agent-toolkit-for-aws --skill agents-buildAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 896 |
|---|---|
| repo stars | ★ 2.2k |
| Last updated | August 4, 2026 |
| Repository | aws/agent-toolkit-for-aws ↗ |
How do I apply agents-build using the workflow in its SKILL.md?
>
Who is it for?
Developers following the agents-build skill for the tasks it documents.
Skip if: Tasks outside the agents-build scope described in SKILL.md.
When should I use this skill?
User mentions agents-build or related triggers from the skill description.
What you get
Working agents-build setup aligned with the documented patterns and constraints.
- agent scaffold
- tool definitions
- deployment config
Files
build
Add capabilities to your AgentCore agent project.
When to use
- Adding cross-session memory to your agent
- Calling your deployed agent from a web app, mobile app, or backend service
- Configuring VPC networking for private resources (RDS, internal APIs)
- Building multi-agent systems with orchestrator/specialist patterns
- Migrating an existing Bedrock Agent to AgentCore
- Adding the Browser tool so the agent can navigate websites
- Adding the Code Interpreter so the agent can execute code in a sandbox
- Removing resources from your project or tearing down a deployment
Do NOT use for:
- Connecting to external tools/APIs via Gateway (OpenAPI specs, Lambda, MCP servers, credentials, policies) → use
agents-connect - Scaffolding a new project → use
agents-get-started - Deploying → use
agents-deploy
Input
$ARGUMENTS can be:
- A capability: "memory", "integrate", "vpc", "multi-agent", "migrate", "browser", "code-interpreter", "teardown"
- A description of what they want: "remember user preferences", "call from React app", "scrape a website", "run pandas in the agent", "delete my agent", "clean up resources"
- Empty — the skill will determine the workflow from context
Process
Step 0: Verify CLI version
Run agentcore --version. This skill requires v0.9.0 or later.
If older: "Run agentcore update to get the latest version."
Step 1: Read project context
Read agentcore/agentcore.json to understand the current project — framework, existing resources, agent configuration.
If agentcore/agentcore.json is not found:
1. Check if the developer is in the wrong directory. Look for agentcore/agentcore.json in parent directories (up to 3 levels). If found, tell them: "Found an AgentCore project at <path>. Are you working in that project?" 2. If no project exists anywhere nearby, ask what capability they wanted to add. Then offer two paths:
- "I can walk you through creating a project first and then adding CAPABILITY — want to do that?" (run the get-started flow inline, then continue with the build workflow)
- "If you already have a project elsewhere,
cdinto it and try again."
Do not just say "go use agents-get-started" and stop — that loses the developer's context about what they actually wanted to do.
Step 2: Determine the workflow
Important disambiguation — before routing to a build reference, check if the prompt is actually a connect or debug concern:
- If the phrase mentions external APIs, Lambda functions, OpenAPI specs, gateways, credentials, MCP servers, or policies → this is
agents-connect, not build - If the developer says something is broken (wrong answers, errors, tool failures) → this is
agents-debug, not build - Build is for adding new capabilities to a working project, not fixing broken ones
Based on the developer's prompt and $ARGUMENTS, load the appropriate reference:
| Developer intent | Reference to load |
|---|---|
| Add memory, remember things, user preferences, cross-session | `references/memory.md` |
| Call agent from app, invoke from code, streaming, SDK client, agent URL, execute shell in session | `references/integrate.md` |
| VPC, private network, RDS, internal API, subnet, security group | `references/vpc.md` |
| Multi-agent, orchestrator, specialist, A2A, delegation, agent handoff | `references/multi-agent.md` |
| Custom headers from caller to agent, header allowlist, tenant ID/correlation ID/trace propagation | `references/request-headers.md` |
| Migrate Bedrock Agent, import agent, move to AgentCore | `references/migrate.md` |
| Browser tool, web navigation, form filling, scraping, Nova Act, Playwright, live view | `references/browser.md` |
| Code Interpreter, execute code, sandbox, run Python/JS/TS, data analysis in agent, pandas | `references/code-interpreter.md` |
| Delete agent, remove resource, tear down, clean up, destroy, start fresh | `references/teardown.md` |
| Change model, switch model, use Haiku/Sonnet/Nova, different model | Inline — see "Changing the model" below |
If the developer asks about the difference between local dev and deployed (e.g., "why does my memory work after deploy but not locally?"), load `references/local-vs-deployed.md` alongside the specific workflow reference.
Read the matching file into context and follow its Process section step by step — do not summarize.
If the intent is ambiguous, ask the developer which capability they want to add.
Changing the model
The model is configured in app/<AgentName>/model/load.py (scaffolded by agentcore create). To change it:
1. Open app/<AgentName>/model/load.py 2. Change the model_id parameter in the BedrockModel() constructor
# Default (scaffolded by CLI)
return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")
# Switch to Haiku for cost savings
return BedrockModel(model_id="us.anthropic.claude-3-5-haiku-20241022-v1:0")
# Switch to Nova Lite
return BedrockModel(model_id="amazon.nova-lite-v1:0")Cross-region inference profile prefixes (us., eu., apac., global.) control where inference runs. Use global. for maximum throughput, or a geographic prefix for data residency. Not all models support all prefixes — check the Bedrock inference profiles docs.
After changing the model:
- Verify the model is enabled in your region: AWS Console → Amazon Bedrock → Model access
- For cross-region profiles, enable in all destination regions
- If using
agents-harden, update the IAM policy to scope to the new model ARN - Run
agentcore devto test locally, thenagentcore deployto update the deployed agent
No agentcore.json change is needed — the model is configured in code, not in the project config.
Pre-flight: validate any --name before generating the CLI command
Whichever reference you load, most end up producing an agentcore add <resource> --name <something> command. The CLI fails late on invalid names — you'll see the error after walking through prompts, not before running the command. Validate up front:
| Resource | Max chars | Allowed | Starts with |
|---|---|---|---|
Agent (add agent) | 48 | alphanumeric + _ | letter |
| Memory, gateway, gateway-target, credential, evaluator, online-eval, policy, policy-engine | 48 | alphanumeric + _ | letter |
Count the characters before constructing the command. If the name is over the limit or contains hyphens, dots, or spaces, push back: "<name> is N characters / uses -, which the CLI rejects. How about <suggestion>?" Never run the command with an invalid name hoping the CLI message will be clear.
Note: agentcore create --name (the project name) has a stricter 23-char limit and does not allow underscores. That's covered in agents-get-started; if you see the developer re-running create, flag the 23-char limit specifically.
Output
Depends on the workflow — see the loaded reference for specific outputs.
Quality criteria
- The correct reference was loaded based on the developer's intent
- All output follows the loaded reference's quality criteria
- Cross-references to other skills (agents-connect, agents-deploy) are included where relevant
browser
Add the AgentCore Browser tool so your agent can navigate web pages, fill forms, and extract information.
When to use
- Your agent needs to interact with a website that has no API
- Your agent needs to fill forms, scrape data, or drive a web app
- You want an isolated, session-scoped browser for the agent (not a shared one)
- You want live-view / recording / replay of what the browser did, for debugging or auditing
Do NOT use this reference for:
- Calling an API — use Gateway (
agents-connect) - Running code in a sandbox — see `code-interpreter.md`
- Serving browser-based UIs to users — that's a different problem (the AGUI protocol, not the Browser tool)
Mental model
The Browser tool is a managed Chrome instance, one per session, running in an isolated microVM. Your agent connects to it over WebSocket (via CDP — Chrome DevTools Protocol) and drives it with an automation framework. You pick the framework:
| Framework | When to use |
|---|---|
| Strands `AgentCoreBrowser` | Agent-driven browsing inside a Strands agent. Highest-level, tool-use-native. |
| Nova Act | You want an LLM to decide the next action at each step ("click the search box, type X, press enter"). Best for open-ended tasks. |
| Playwright | Deterministic scripted automation. Best when you know the exact steps — login flows, scraping a known page structure. |
If you're adding browsing to a Strands agent, use AgentCoreBrowser and skip the framework decision — it wraps Nova Act under the hood and fits the agent-tool mental model.
If you're not using Strands, pick between Nova Act (reasoning-driven) and Playwright (script-driven) based on whether the task is open-ended or well-defined.
Sessions are ephemeral by default (reset after each use). Default timeout is 15 minutes, max 8 hours. You can run multiple concurrent sessions.
Prerequisites
- Python 3.10+
bedrock-agentcoreSDK installed- IAM permissions for
bedrock-agentcore:*Browser*actions (scope to your browser resource ARN in production) - AWS region that supports Browser — check the docs for the current list
- For Strands path: model access for your chosen model (Claude Sonnet 4.x is the common default)
- For Nova Act path: a Nova Act API key from nova.amazon.com/act (US-based amazon.com accounts only at time of writing)
IAM policy skeleton (attach to the caller identity — your user, role, or AgentCore Runtime execution role):
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "BrowserAccess",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreateBrowser",
"bedrock-agentcore:GetBrowser",
"bedrock-agentcore:ListBrowsers",
"bedrock-agentcore:StartBrowserSession",
"bedrock-agentcore:StopBrowserSession",
"bedrock-agentcore:GetBrowserSession",
"bedrock-agentcore:ListBrowserSessions",
"bedrock-agentcore:ConnectBrowserAutomationStream",
"bedrock-agentcore:ConnectBrowserLiveViewStream"
],
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:browser/*"
}]
}Check current IAM action names against the docs — the list evolves.
Path A — Strands agent with the Browser tool (recommended for most)
from strands import Agent
from strands_tools.browser import AgentCoreBrowser
browser_tool = AgentCoreBrowser(region="<REGION>")
agent = Agent(tools=[browser_tool.browser])
result = agent("Find the release date of the latest AgentCore SDK on GitHub.")
print(result.message["content"][0]["text"])Install: pip install bedrock-agentcore strands-agents strands-agents-tools
The agent decides when to use the browser, opens sessions on demand, and cleans them up. Under the hood, AgentCoreBrowser uses the AWS-managed aws.browser.v1 resource — no resource creation needed.
Dropping into an AgentCore Runtime entrypoint:
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from strands_tools.browser import AgentCoreBrowser
from model.load import load_model # scaffolded by `agentcore create`
import os
app = BedrockAgentCoreApp()
REGION = os.getenv("AWS_REGION", "us-west-2")
@app.entrypoint
def invoke(payload, context):
browser_tool = AgentCoreBrowser(region=REGION)
agent = Agent(model=load_model(), tools=[browser_tool.browser])
result = agent(payload.get("prompt", ""))
return {"response": str(result)}
if __name__ == "__main__":
app.run()Path B — Nova Act for reasoning-driven tasks
Use when the task needs an LLM to decide each click/type step.
from bedrock_agentcore.tools.browser_client import browser_session
from nova_act import NovaAct
def run_browser_task(prompt: str, starting_page: str, nova_act_key: str, region: str = "us-west-2"):
with browser_session(region) as client:
ws_url, headers = client.generate_ws_headers()
with NovaAct(
cdp_endpoint_url=ws_url,
cdp_headers=headers,
nova_act_api_key=nova_act_key,
starting_page=starting_page,
) as nova:
return nova.act(prompt)Install: pip install bedrock-agentcore nova-act boto3
The browser_session context manager handles start/stop. Do not leak sessions — always use the context manager or wrap raw BrowserClient calls in try/finally.
Credential handling: the Nova Act API key is a secret. If this is running inside an AgentCore Runtime agent, register it as a credential (agentcore add credential --name NovaAct --api-key ...) and retrieve it with @requires_api_key(provider_name="NovaAct"). Do not put it in runtime env vars. See agents-connect Path D.
Path C — Playwright for scripted automation
Use when the steps are fixed and you want deterministic behavior (logins, scrapes, automated tests).
import asyncio
from bedrock_agentcore.tools.browser_client import browser_session
from playwright.async_api import async_playwright
async def scrape_title(url: str, region: str = "us-west-2") -> str:
async with async_playwright() as pw:
with browser_session(region) as client:
ws_url, headers = client.generate_ws_headers()
browser = await pw.chromium.connect_over_cdp(ws_url, headers=headers)
context = browser.contexts[0]
page = context.pages[0]
try:
await page.goto(url)
return await page.title()
finally:
await page.close()
await browser.close()
print(asyncio.run(scrape_title("https://example.com")))Install: pip install bedrock-agentcore playwright
Sync variant (sync_playwright) is also supported — pick based on whether your agent code is async.
Observability
Browser is observable by default:
- Live view — watch a running session in real time from the AWS console (Built-in tools → Browser → your session → "View live session"). You can take over control from the automation interactively.
- CloudWatch logs —
/aws/bedrock-agentcore/browser/* - Metrics — in
AWS/BedrockAgentCorenamespace
Session recording (DOM, clicks, console logs, network) is opt-in per browser. To enable:
1. Create a custom browser (not aws.browser.v1) with recording configured 2. Give its execution role s3:PutObject on your recording bucket 3. Recordings land in your S3 bucket and replay in the AWS console
The managed aws.browser.v1 resource does not record. Use custom browsers when you need audit trails.
Session lifecycle — always close
# Right — context manager
with browser_session(region) as client:
ws_url, headers = client.generate_ws_headers()
...
# Also right — explicit try/finally
client = BrowserClient(region=region)
client.start()
try:
...
finally:
client.stop()
# Wrong — leaked session
client = BrowserClient(region=region)
client.start()
... # if this raises, the session sits idle until its 15-minute timeoutSessions hold a microVM. Leaked sessions cost money until they time out. The context manager is non-negotiable for production.
VPC mode
If your agent runs in VPC mode, the Browser tool can also run in VPC. See `vpc.md` for the subnet + security group pattern (the same service-linked role covers Browser ENIs). Browser in VPC requires a NAT gateway for public-internet sites — public subnets don't give Browser internet access.
Common failures
"Access denied" starting a session: IAM is missing StartBrowserSession on the browser resource ARN. Check aws sts get-caller-identity matches the identity you attached the policy to.
"Model access denied" from a Strands agent: The browser tool itself is fine, but the agent's model isn't enabled. Go to Bedrock console → Model access → enable your model in the region.
Nova Act errors about API key: The key is US-amazon.com-accounts only at launch. If you're outside the US or using a work account, you can't use Nova Act yet — fall back to Playwright or Strands.
Browser session times out mid-task: Default is 15 minutes of idle time. Pass sessionTimeoutSeconds to StartBrowserSession (max 28800 = 8 hours). Don't use this to cover up agents that are slow — fix the agent or chunk the work.
Live view doesn't show your session: Live view requires ConnectBrowserLiveViewStream IAM permission. The session also has to be Ready, not Starting or Stopping.
Output
- Which framework fits (Strands vs Nova Act vs Playwright)
- Working code with session lifecycle handled
- IAM policy scoped to the browser resource
- Observability setup if needed (live view, recording)
Quality criteria
- Browser sessions are always wrapped in a context manager or try/finally — never leaked
- IAM is scoped to
browser/*in the account, notResource: "*" - Nova Act API keys and other secrets use
agentcore add credential+@requires_api_key, not env vars - The code handles the case where the agent runs outside AgentCore Runtime (no
.env.local, no credential provider) — typically by reading a local secret for development and the credential provider for production
code-interpreter
Add the AgentCore Code Interpreter tool so your agent can execute code in a sandboxed environment — Python, JavaScript, or TypeScript.
When to use
- Your agent needs to run math, data analysis, or transform data where a calculation is more reliable than an LLM answer
- Your agent generates code as an answer and you want it executed (and its output verified) before returning
- Your agent needs to read/write files (CSV, JSON, plots) that should persist to S3
- You need an isolated, session-scoped code sandbox
Do NOT use this reference for:
- Interacting with web pages — see `browser.md`
- Running arbitrary long-lived services — Code Interpreter is for short-lived code execution, not hosting servers
- Shell commands inside your live agent session's own microVM — that's
InvokeAgentRuntimeCommand, covered in `integrate.md`
Mental model
Code Interpreter is a managed sandbox, one per session, running in an isolated microVM. Your code can:
- Execute Python, JavaScript, or TypeScript
- Read/write files on a local filesystem (up to 100 MB inline upload, up to 5 GB via S3)
- Make network calls (if internet access is enabled on the resource)
- Use pre-installed libraries (pandas, numpy, scikit-learn, torch, etc. — see docs for the current list)
Sessions are stateful within a session (variables and files persist across execute_code calls in the same session) and ephemeral across sessions (start a new session and the filesystem is clean).
Prerequisites
- Python 3.10+ in your agent environment
bedrock-agentcoreSDK- IAM permissions for
bedrock-agentcore:*CodeInterpreter*actions, scoped to the resource ARN - Model access if calling via an agent framework (the framework calls a model to decide when to execute code)
IAM policy skeleton:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "CodeInterpreterAccess",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreateCodeInterpreter",
"bedrock-agentcore:GetCodeInterpreter",
"bedrock-agentcore:ListCodeInterpreters",
"bedrock-agentcore:StartCodeInterpreterSession",
"bedrock-agentcore:StopCodeInterpreterSession",
"bedrock-agentcore:InvokeCodeInterpreter",
"bedrock-agentcore:GetCodeInterpreterSession",
"bedrock-agentcore:ListCodeInterpreterSessions"
],
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:code-interpreter/*"
}]
}Check current action names against the docs — the list evolves.
Path A — Strands agent with Code Interpreter (recommended for most)
from strands import Agent
from strands_tools.code_interpreter import AgentCoreCodeInterpreter
tool = AgentCoreCodeInterpreter(region="<REGION>")
agent = Agent(
tools=[tool.code_interpreter],
system_prompt=(
"You are an assistant that validates claims with code. "
"When asked to compute, calculate, or analyze, write Python and run it."
),
)
result = agent("What are the first 10 Fibonacci numbers?")
print(result.message["content"][0]["text"])Install: pip install bedrock-agentcore strands-agents strands-agents-tools
The agent decides when to execute code, starts sessions on demand, and stops them. Under the hood, the tool uses the AWS-managed aws.codeinterpreter.v1 resource — no resource creation needed.
Dropping into an AgentCore Runtime entrypoint:
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from strands_tools.code_interpreter import AgentCoreCodeInterpreter
from model.load import load_model
import os
app = BedrockAgentCoreApp()
REGION = os.getenv("AWS_REGION", "us-east-1")
@app.entrypoint
def invoke(payload, context):
tool = AgentCoreCodeInterpreter(region=REGION)
agent = Agent(
model=load_model(),
tools=[tool.code_interpreter],
system_prompt="Validate computations with code.",
)
return {"response": str(agent(payload.get("prompt", "")))}
if __name__ == "__main__":
app.run()Path B — Direct SDK for programmatic execution
Use when your code — not an agent — decides what to run. Good for ETL, data transformation, and agent-internal validation.
from bedrock_agentcore.tools.code_interpreter_client import code_interpreter_session
REGION = "us-east-1"
with code_interpreter_session(REGION) as session:
# Stateful: variables persist across calls within the session
session.execute_code("import pandas as pd")
session.execute_code("df = pd.DataFrame({'x': [1, 2, 3]})")
result = session.execute_code("df.describe().to_string()")
print(result.stdout)The context manager handles start/stop. Do not leak sessions.
Language selection — default is Python. For JavaScript/TypeScript, pass language="javascript" or language="typescript" to execute_code (or the runtime setting at session start). See the runtime selection doc for the current supported runtimes.
Path C — Custom Code Interpreter with S3 access
The managed aws.codeinterpreter.v1 resource has no S3 write permissions. For agents that produce artifacts (plots, reports, processed datasets) you want to persist, create a custom Code Interpreter with an execution role that has S3 access.
This is a CreateCodeInterpreter call (SDK/API, not exposed via agentcore CLI at time of writing). The execution role's trust policy grants bedrock-agentcore.amazonaws.com the ability to assume it, and its permissions policy grants s3:PutObject and related actions on your artifact bucket. Check the docs for the current CreateCodeInterpreter shape and the exact trust policy format.
Same-account S3 rule. The S3 bucket must be in the same AWS account as the Code Interpreter resource. Cross-account buckets are not supported as targets even with the right bucket policy — CreateCodeInterpreter fails with a validation error. If you need the artifacts in another account, replicate from the same-account bucket afterward.
Observability
- CloudWatch logs — stdout/stderr from executed code, plus session lifecycle events
- CloudTrail — every
StartCodeInterpreterSession,InvokeCodeInterpreter,StopCodeInterpreterSessioncall - Metrics — in
AWS/BedrockAgentCorenamespace
Pre-installed libraries
The managed Python runtime includes: pandas, numpy, scipy, matplotlib, plotly, scikit-learn, torch, torchvision, statsmodels, and dozens more for data analysis / ML. Check the current list in the docs before telling a user "library X is preinstalled" — the list changes with platform updates.
For libraries not preinstalled, call install_packages(["your-lib==1.2"]) in your session (or !pip install ... via execute_command). Installed packages last only for the session.
Session lifecycle — always close
# Right — context manager
with code_interpreter_session(region) as session:
session.execute_code("...")
# Right — try/finally with explicit client
client = CodeInterpreterClient(region=region)
client.start()
try:
client.execute_code("...")
finally:
client.stop()
# Wrong — leaked session sits until timeoutDefault session timeout is 900 seconds (15 min), max 28800 seconds (8 hours). Leaked sessions cost money.
VPC mode
Code Interpreter supports VPC — same pattern as Runtime and Browser (service-linked role, your subnets, your security group). See `vpc.md`.
Public internet from the sandbox requires a NAT gateway on a private subnet, same as Runtime. Public subnets don't give Code Interpreter ENIs internet access. If the code needs pip install to reach PyPI, plan for NAT.
Common failures
"Access denied" on StartCodeInterpreterSession: IAM missing the action on the resource ARN. Use aws sts get-caller-identity to confirm which identity you attached the policy to.
"ValidationException: Role does not have access to required S3 buckets": S3 bucket is in a different account. Move the bucket or replicate from an in-account staging bucket.
Code times out: Default execute timeout is short. Split long jobs into chunks, or use a custom Code Interpreter with extended timeouts. Don't try to run 30-minute training jobs in Code Interpreter — that's a SageMaker / Batch job.
"Module not found" despite being listed as preinstalled: The preinstalled list may differ between python and nodejs runtimes. Verify runtime selection and list matches.
Output
- Which path fits (Strands tool vs direct SDK vs custom with S3)
- Working code with session lifecycle handled
- IAM policy scoped to the code-interpreter resource
Quality criteria
- Sessions are always wrapped in a context manager or try/finally — never leaked
- IAM is scoped to
code-interpreter/*in the account, notResource: "*" - S3 destination buckets are in the same account as the Code Interpreter resource
- Language / runtime selection is explicit when the code isn't Python
integrate
Help a developer call their deployed agent from an application.
When to use
- Developer has a deployed agent and wants to call it from their app
- Developer needs the agent URL and auth credentials
- Developer wants to handle streaming responses from the agent
- Developer needs to manage conversation sessions across multiple calls
- Developer is building a frontend, backend service, or CLI that consumes the agent
- Caller and agent are in different AWS accounts (cross-account invocation)
Do NOT use for:
- Giving the agent tools to call external APIs → use
agents-connect - Deploying the agent → use
agents-deploy - Debugging agent responses → use
agents-debug - Securing the agent endpoint for production → use
agents-harden(but this skill covers the client-side auth code)
Input
$ARGUMENTS can be:
- A language or framework: "from React", "in Python", "Node.js backend"
- An auth preference: "using IAM", "with JWT"
- Empty — the skill will detect the project context and guide accordingly
Process
Step 1: Check deployment status
Read agentcore/agentcore.json to get the agent name. Then check if it's deployed:
agentcore status --type agentIf not deployed: "Your agent needs to be deployed before you can call it from an app. Run agentcore deploy first, or use the agents-deploy skill for guidance."
Do not proceed until the agent is deployed.
Step 2: Get the agent endpoint
agentcore fetch access --name <AgentName> --type agentThis returns:
- Agent Runtime ARN — needed for SDK invocation
- Endpoint URL — for direct HTTPS calls
- Auth configuration — what auth method is configured
Note the auth type from the output. It determines how the client app authenticates.
Step 3: Determine auth method
Read the agent's authorizerType field from agentcore/agentcore.json (it's a top-level field on the runtime entry; JWT details live in the separate authorizerConfiguration object on the same runtime).
| Auth type | How the client authenticates | Best for |
|---|---|---|
| None (default) | IAM SigV4 signing on the request | Backend services with AWS credentials |
| AWS_IAM | IAM SigV4 signing on the request | Backend services, Lambda-to-agent calls |
| CUSTOM_JWT | Bearer token in Authorization header | Web/mobile apps with an identity provider |
If no authorizer is configured: The agent uses IAM auth by default. The calling identity needs bedrock-agentcore:InvokeAgentRuntime permission.
If CUSTOM_JWT: The client sends a JWT from the configured identity provider. The agent validates it against the discovery URL, allowed audience, and allowed clients configured during setup.
Step 4: Generate client code
Based on the developer's language preference (from $ARGUMENTS or ask), generate the appropriate client code.
Python (boto3) — IAM auth
import boto3
import json
from botocore.exceptions import ClientError
client = boto3.client("bedrock-agentcore", region_name="<REGION>")
try:
response = client.invoke_agent_runtime(
agentRuntimeArn="<AGENT_RUNTIME_ARN>",
qualifier="DEFAULT", # or a specific version number
payload=json.dumps({
"prompt": "Hello, what can you do?"
}).encode(),
runtimeSessionId="session-123", # reuse for multi-turn conversations
)
# Handle streaming response — response["response"] is a StreamingBody
stream = response["response"]
if hasattr(stream, "iter_lines"):
for line in stream.iter_lines():
if line:
print(line.decode(), end="", flush=True)
else:
# Some SDK versions return raw bytes — read all at once
content = stream.read()
print(content.decode() if isinstance(content, bytes) else content)
except ClientError as e:
code = e.response["Error"]["Code"]
if code == "AccessDeniedException":
# Missing bedrock-agentcore:InvokeAgentRuntime permission
raise RuntimeError("Caller lacks InvokeAgentRuntime permission") from e
elif code == "ValidationException":
# Wrong ARN, bad payload format, invalid session ID
raise RuntimeError(f"Invalid request: {e}") from e
elif code == "ThrottlingException":
# Retry with exponential backoff
raise
else:
raisePython (HTTPS) — JWT auth
import requests
AGENT_URL = "<ENDPOINT_URL>"
JWT_TOKEN = "<TOKEN_FROM_YOUR_IDP>"
response = requests.post(
AGENT_URL,
headers={
"Authorization": f"Bearer {JWT_TOKEN}",
"Content-Type": "application/json",
},
json={"prompt": "Hello, what can you do?"},
stream=True,
)
for chunk in response.iter_content(chunk_size=None):
print(chunk.decode(), end="", flush=True)JavaScript/TypeScript (AWS SDK) — IAM auth
import {
BedrockAgentCoreClient,
InvokeAgentRuntimeCommand,
} from "@aws-sdk/client-bedrock-agentcore";
const client = new BedrockAgentCoreClient({ region: "<REGION>" });
const response = await client.send(
new InvokeAgentRuntimeCommand({
agentRuntimeArn: "<AGENT_RUNTIME_ARN>",
qualifier: "DEFAULT",
payload: new TextEncoder().encode(
JSON.stringify({ prompt: "Hello, what can you do?" })
),
runtimeSessionId: "session-123",
})
);
// response.response is the streaming body
const decoder = new TextDecoder();
for await (const chunk of response.response) {
process.stdout.write(decoder.decode(chunk));
}JavaScript/TypeScript (fetch) — JWT auth
const AGENT_URL = "<ENDPOINT_URL>";
const JWT_TOKEN = "<TOKEN_FROM_YOUR_IDP>";
const response = await fetch(AGENT_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${JWT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ prompt: "Hello, what can you do?" }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value));
}Step 5: Session management
Explain how sessions work:
- `runtimeSessionId` — pass the same value across multiple calls to maintain conversation context
- Generate a unique session ID per user conversation (e.g., UUID)
- Sessions are server-side — the agent remembers the conversation history for that session ID
- If you omit the session ID, each call is stateless (no conversation memory)
import uuid
# New conversation
session_id = str(uuid.uuid4())
# First turn
invoke(session_id, "What's the weather in Seattle?")
# Follow-up in same conversation
invoke(session_id, "What about tomorrow?")
# New conversation — new session
new_session_id = str(uuid.uuid4())
invoke(new_session_id, "Different topic entirely")Step 6: Protocol-specific guidance
Read the agent's protocol from agentcore/agentcore.json.
If HTTP (default): The patterns above apply directly.
If MCP: The agent exposes an MCP endpoint. Clients connect using the MCP protocol (Streamable HTTP). Point the developer to MCP client libraries for their language.
If A2A: The agent exposes an Agent-to-Agent endpoint with a card at /.well-known/agent-card.json. The calling agent discovers capabilities via the card and communicates over JSON-RPC 2.0. See `references/multi-agent.md` in this skill for A2A integration patterns.
Step 7: Integration patterns that look right but fail
Two patterns come up often enough in support cases to call out directly.
API Gateway `/{proxy+}` with a URL-encoded Runtime ARN. Fronting AgentCore Runtime with an API Gateway REST API whose resource is /{proxy+} and whose integration URI is the encoded runtime ARN appears to work — the deploy succeeds and short requests return. Longer requests fail at around 2 minutes with Integration closed connection prematurely in the logs, regardless of integrationTimeoutInMillis. HTTP_PROXY is a generic forwarding integration; it doesn't handle SigV4, streaming, or session semantics the way the SDK client does.
Use one of these instead:
- Call Runtime directly from the client with the
bedrock-agentcoreSDK (Step 4 above). This is the intended path. - Put a Lambda between API Gateway and Runtime if you need API Gateway for rate limiting, a public HTTPS endpoint, or other reasons. The Lambda receives the request, calls
invoke_agent_runtime, and streams the response back. The Lambda's execution role needsbedrock-agentcore:InvokeAgentRuntime. Be aware that API Gateway has a 29-second hard ceiling on synchronous responses — this works only for fast agents. For anything multi-step, use the direct SDK path instead.
Lambda-in-front for synchronous agent responses hits a short timeout ceiling. A Client → API Gateway → Lambda → Runtime chain caps at ~29 seconds because of the API Gateway synchronous response limit. Any agent that reasons, calls multiple tools, or uses a non-trivial model will exceed it. If you're hitting timeouts on a Lambda wrapping Runtime, the fix is usually to drop the Lambda and let the client call Runtime directly — Runtime supports streaming responses natively, which is typically the reason teams add a Lambda in the first place.
Step 8: Cross-account invocation
Calling an agent in a different AWS account than your caller uses standard AWS cross-account IAM patterns — no AgentCore-specific plumbing. The caller account assumes a role in the agent's account, gets temporary credentials, and uses them to sign the invoke request.
Setup in the agent's account (Account B):
Create an IAM role that trusts the caller account and has permission to invoke the runtime.
// Trust policy — who can assume this role
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::<CALLER_ACCOUNT_ID>:root"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"sts:ExternalId": "<unique-external-id>"}
}
}]
}// Permissions policy — what this role can do
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "bedrock-agentcore:InvokeAgentRuntime",
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<AGENT_ACCOUNT_ID>:runtime/<RUNTIME_NAME>-*"
}]
}Scope the Principal in the trust policy as narrowly as possible (a specific role ARN in the caller account rather than :root for anything beyond proof-of-concept). Use an ExternalId to prevent the confused deputy problem.
In the caller's app (Account A):
import boto3
import json
# Assume the role in Account B
sts = boto3.client("sts")
assumed = sts.assume_role(
RoleArn="arn:aws:iam::<AGENT_ACCOUNT_ID>:role/<ROLE_NAME>",
RoleSessionName="agent-invoker",
ExternalId="<unique-external-id>",
)
creds = assumed["Credentials"]
# Use the temporary credentials to invoke the runtime
agentcore = boto3.client(
"bedrock-agentcore",
region_name="<REGION>",
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"],
)
response = agentcore.invoke_agent_runtime(
agentRuntimeArn="arn:aws:bedrock-agentcore:<REGION>:<AGENT_ACCOUNT_ID>:runtime/<RUNTIME_NAME>",
qualifier="DEFAULT",
payload=json.dumps({"prompt": "hello"}).encode(),
runtimeSessionId="session-123",
)Production notes:
- Cache the assumed-role credentials. They're valid for the session duration (default 1 hour). Re-assume when they're close to expiring, not on every request.
- Boto3's
Sessionwith a profile usingrole_arnandsource_profilecan automate this if your caller environment supports AWS config profiles.assume_rolein code is the explicit version. - If the caller is in a Lambda, ECS task, or EC2 instance, the execution/task role is what gets the AssumeRole permission. That role's trust policy is what gets listed in Account B's trust policy.
- The runtime's own resource policy (if any) is separate from IAM. Typically you don't need a resource policy for cross-account — the IAM role in Account B is what grants access.
Running shell commands inside a live agent session (InvokeAgentRuntimeCommand)
Once an agent's session is running, you can execute shell commands inside that same session's microVM — same filesystem, same env, same network namespace — and stream the output back. This sits alongside InvokeAgentRuntime (which drives the agent's reasoning loop), not in place of it.
When this is useful:
- Coding/devops agents where your app runs deterministic ops (git pull, build, test, file system inspection) instead of asking the LLM to reason about them
- Seeding the session's filesystem before the agent runs (drop a dataset into
/tmp, then invoke the agent to analyze it) - Debugging a stuck or misbehaving session — run
ps,ls,cat /tmp/logfrom outside without going through the agent - Any workflow where you want the reliability of a scripted command and the context of a warm session
When it's the wrong tool:
- Spawning new sessions to run arbitrary code for users — use the `code-interpreter.md` built-in tool instead; it's purpose-built, sandboxed differently, and doesn't consume an agent's session
- Running anything an unrelated caller shouldn't be able to do — commands execute with the runtime's execution role and filesystem
IAM permission required: bedrock-agentcore:InvokeAgentRuntimeCommand on the runtime ARN. This is a separate action from InvokeAgentRuntime — scope it explicitly to the callers who need it.
import boto3
client = boto3.client("bedrock-agentcore", region_name="<REGION>")
response = client.invoke_agent_runtime_command(
agentRuntimeArn="<AGENT_RUNTIME_ARN>",
qualifier="DEFAULT",
runtimeSessionId="session-123", # must be an existing session
command="ls -la /tmp && cat /tmp/status.json",
)
# Output streams back over HTTP/2 on response["response"]
for chunk in response["response"].iter_chunks():
print(chunk.decode(), end="", flush=True)Session must exist. InvokeAgentRuntimeCommand attaches to a running session; it won't create one. If the session has expired or never existed, the call fails. Invoke the agent first (to start the session), then use the session ID for subsequent command calls.
Same microVM, same filesystem. A file written by the command is visible to the agent on the next invoke, and vice versa. Use this to pre-load artifacts, then reason over them in the agent. Session isolation still applies — other sessions cannot see these files.
[!WARNING]
InvokeAgentRuntimeCommand executes arbitrary shell commands inside a live agent
session with the runtime's full execution role. Never grant
bedrock-agentcore:InvokeAgentRuntimeCommand to the same principals that have
bedrock-agentcore:InvokeAgentRuntime unless they explicitly need shell access.
Always create a separate IAM policy for command execution. Always enable CloudTrail
logging for InvokeAgentRuntimeCommand calls. If commands are constructed from
user-supplied input, validate and sanitize — this is a command injection surface.
IAM separation: InvokeAgentRuntimeCommand is a distinct IAM action from InvokeAgentRuntime. Grant it only to the callers that need shell access — not to every identity that can invoke the agent. Minimal example:
{
"Effect": "Allow",
"Action": "bedrock-agentcore:InvokeAgentRuntimeCommand",
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<YOUR_ACCOUNT_ID>:runtime/<RUNTIME_NAME>-*"
}Keep this in a separate IAM policy from the one that grants InvokeAgentRuntime. Attach it only to roles that explicitly need to run commands inside agent sessions.
Command injection: The code example above uses a hardcoded command string — intentionally. If your real usage constructs commands from user-supplied input, validate before passing: reject strings containing &&, ;, $(...), backticks, |, or other shell metacharacters. Passing unsanitized user input to InvokeAgentRuntimeCommand is a direct code execution vulnerability.
CloudTrail monitoring: Enable an EventBridge rule to alert on unexpected InvokeAgentRuntimeCommand calls:
aws events put-rule \
--name AgentCoreCommandExecution \
--event-pattern '{"source":["aws.bedrock-agentcore"],"detail-type":["AWS API Call via CloudTrail"],"detail":{"eventName":["InvokeAgentRuntimeCommand"]}}' \
--state ENABLEDA compromised caller with this permission can read/write the agent's filesystem, reach any network resource the agent can reach, and use the execution role's credentials — CloudTrail logging is the minimum detection baseline.
Reference integrations
Two common integration targets have published, reusable patterns you can start from instead of building the integration layer yourself.
Slack. Integrating Amazon Bedrock AgentCore with Slack walks through a reusable integration layer that brings any AgentCore agent into a Slack workspace. The architecture (API Gateway → Lambda → SQS → AgentCore) handles Slack's 3-second webhook timeout via asynchronous processing: one Lambda validates the Slack signature and returns immediately, another posts a "Processing..." placeholder, and a third invokes the agent and replaces the placeholder with the real response. The pattern maps Slack thread timestamps to AgentCore Memory session IDs and Slack user IDs to actor IDs, so conversation context persists in the same thread over time. The integration layer is decoupled from the agent — you swap in any agent (FinOps, DevOps, incident response) without touching the Slack infrastructure. Deploys with one cdk deploy.
Microsoft Teams. The same async-processing architecture (API Gateway → Lambda → queue → AgentCore) applies to Teams. See How Amazon Bedrock transforms Microsoft Teams conversations into actionable insights for Teams-specific setup (Bot Framework registration, bot channel configuration). If you've already built the Slack pattern above, the Teams version is primarily a different webhook validator and response formatter.
Both patterns handle the "webhook platform with short timeout" problem in the same way — the chat platform gets an immediate ack and a placeholder, the real agent call happens asynchronously, and the response replaces the placeholder when ready. If you're integrating a third chat platform not listed here, use either blog as a template.
Output
- The agent's endpoint URL and ARN
- Auth method explanation with client-side code
- Working client code in the developer's preferred language
- Session management guidance
- Protocol-specific notes if applicable
Quality criteria
- Client code uses the correct SDK client (
bedrock-agentcore, notbedrock-agent) - Auth method matches what's configured on the agent
- Streaming response handling is included (not just request/response)
- Session ID pattern is explained
- Code is complete and runnable — includes imports, error handling basics
Local vs. Deployed — What Works Where
AgentCore has a local dev server (agentcore dev) and a deployed runtime. They don't have feature parity. This reference tells you what works where so generated code and troubleshooting handle both environments correctly.
Quick reference
| Feature | agentcore dev (local) | Deployed runtime |
|---|---|---|
| Agent invocation | ✅ via curl on localhost:8080 | ✅ via invoke_agent_runtime or HTTPS |
| Framework model calls | ✅ if Bedrock creds are available | ✅ |
| Python/JS function tools (framework-native) | ✅ | ✅ |
Credentials (@requires_api_key, @requires_access_token) | ✅ from agentcore/.env.local | ✅ from Secrets Manager |
| Memory | ❌ env var not set locally | ✅ MEMORY_<NAME>_ID injected |
| Gateway | ❌ env var not set locally | ✅ AGENTCORE_GATEWAY_<NAME>_URL injected |
| Cedar policy evaluation | ❌ policies only enforced at gateway | ✅ |
| Traces (X-Ray) | ✅ agentcore dev emits OTEL to CloudWatch by default; disable with --no-traces | ✅ auto-enabled |
| CloudWatch logs | ✅ via ADOT / OTEL wiring (same path as traces) | ✅ if using logging module + OTEL |
*Evaluator definition*** (agentcore add evaluator, writing the instructions/code) | ✅ — writes to agentcore.json; custom code is unit-testable locally | ✅ |
| `agentcore run eval` (on-demand eval over traces) | ✅ — operates on CloudWatch spans; local-dev spans land there if OTEL is on (default) | ✅ |
| `Evaluate` API with hand-constructed spans (boto3) | ✅ — no runtime needed at all; submit SessionSpans directly | ✅ |
Dataset runner (OnDemandEvaluationDatasetRunner) | ❌ invokes an AgentCore Runtime agent in its pipeline | ✅ |
Online eval monitoring (agentcore add online-eval) | ❌ ingests traces continuously from deployed runtime | ✅ |
| Observability dashboards | ✅ once Transaction Search is on and local spans are flowing | ✅ in CloudWatch console |
| VPC networking | ❌ local always has internet | ✅ subject to networkMode: VPC |
| Inbound auth (AWS_IAM, CUSTOM_JWT) | ❌ no auth required locally | ✅ enforced on every request |
Implications for generated code
Always guard features that aren't available locally:
# Memory pattern
MEMORY_ID = os.getenv("MEMORY_MYMEMORY_ID")
if MEMORY_ID:
# deployed — wire up memory
session_manager = AgentCoreMemorySessionManager(...)
else:
# local — agent runs without memory
session_manager = None# Gateway pattern
GATEWAY_URL = os.getenv("AGENTCORE_GATEWAY_WEATHER_URL")
if GATEWAY_URL:
# deployed — use gateway tools
tools = get_gateway_tools(GATEWAY_URL)
else:
# local — agent runs without external tools or with local stubs
tools = []Credentials work in both, but read from different sources. The @requires_api_key decorator handles this automatically — don't try to read env vars directly.
Testing workflow
Because memory, gateway, and policies don't work locally, the realistic test loop is:
1. Local: agentcore dev to verify the agent's code structure, framework wiring, system prompt, and any in-code logic 2. Deploy to a staging target: agentcore deploy --target staging to test with real memory, gateway, and policies 3. Production: only after staging validation
Don't expect agentcore dev to reproduce a production failure involving memory recall, gateway tool calls, or policy denials — those require a deployed environment.
Common "works locally, fails deployed" causes
- Missing
MEMORY_<NAME>_IDguard — code crashes because the env var is unexpectedly present - Hardcoded localhost URLs for gateway — replace with
AGENTCORE_GATEWAY_<NAME>_URL - IAM permissions that work for your dev credentials but not the execution role
- Region mismatch between
aws configure(used locally) andaws-targets.json(used in deploy) - Tool call auth that works with your personal credentials but not with gateway SigV4 from the execution role
Common "works deployed, fails locally" causes
- Code that assumes memory/gateway env vars are always set
- Direct SDK calls that expect the deployed execution role's permissions
- Hardcoded deployed-only URLs or ARNs
memory
Add, configure, and debug AgentCore Memory — the managed service that lets your agent remember things across sessions.
When to use
- You want your agent to remember user preferences, facts, or conversation history across separate sessions
- You added memory via
agentcore createoragentcore add memoryand need to wire it into your agent code - Memory recall isn't working as expected
- You want to share memory across multiple agents
Do NOT use this skill for within-session conversation history. That's handled automatically by the runtime — no configuration needed.
Input
$ARGUMENTS is optional. If provided, use it as the memory resource name:
/memory # uses name from agentcore.json, or prompts
/memory UserContext # targets a specific memory resource by nameProcess
Step 1: Read the project
Read agentcore/agentcore.json. Look for:
- The
memoriesarray — is memory already configured? - The
runtimesarray — what agents are in the project and what framework do they use? - The project
name— needed for env var construction
If `agentcore/agentcore.json` does not exist, check if there's any AgentCore project structure nearby (look for agentcore/ directory). If none found, proceed with the most helpful answer possible based on what the developer asked — don't block on missing context. If the question is about strategy selection or code patterns, answer it directly. Only ask "which situation are you in?" if the answer genuinely depends on it (e.g., they need CLI commands that differ by setup type).
Step 2: Determine the situation
Case A — No memory configured yet The memories array is empty or missing. Proceed to Step 3 (strategy selection).
Case B — Memory configured, needs wiring Memory exists in agentcore.json but the agent code doesn't use it yet. Skip to Step 5 (generate wiring code).
Case C — Memory configured and wired, debugging recall Ask: "What's happening? What did you expect the agent to remember, and what did it actually do?" Then diagnose using the patterns in the Debugging section below.
Case D — Developer asking about memory without a project Answer the question directly. For strategy questions, explain the options. For code questions, show the pattern with a note that they'll need to substitute their actual memory ID.
Step 3: Choose a strategy
Present the options and ask the developer which fits their use case. Don't skip this — the wrong strategy wastes money and produces worse results.
Which memory strategy fits your use case?
SEMANTIC
Best for: remembering facts about users across sessions
How it works: extracts facts and stores them as embeddings; retrieves
relevant context via similarity search at session start
Cost: higher (embedding model + vector search per session)
Example: "Remember that Alex prefers bullet points and works in fintech"
USER_PREFERENCE
Best for: remembering explicit settings and preferences
How it works: extracts structured preference data; optimized for
key-value retrieval
Cost: lower (structured extraction, no vector search)
Example: "Remember my preferred response format and language"
SUMMARIZATION
Best for: remembering what you talked about last time
How it works: compresses conversation history into summaries; injects
the summary at the start of each new session
Cost: medium (summarization model runs at session end)
Example: "Pick up where we left off last time"
EPISODIC
Best for: remembering sequences of events or interactions over time
How it works: stores episodic records of interactions with temporal
context
Cost: medium
Common combinations:
SEMANTIC + USER_PREFERENCE → facts + preferences (most common)
SEMANTIC + SUMMARIZATION → full episodic memory (highest capability, highest cost)
USER_PREFERENCE alone → lightweight preference store
Which strategy (or combination) do you want?Step 4: Add memory to agentcore.json
Run the CLI command to add memory to the project config:
agentcore add memory --name <MemoryName> --strategies <STRATEGY1,STRATEGY2> --expiry 30This updates agentcore/agentcore.json. The memory resource is provisioned when you next run agentcore deploy — it takes 2–5 minutes to become active.
The resulting config entry looks like:
{
"memories": [{
"type": "AgentCoreMemory",
"name": "MyMemory",
"eventExpiryDuration": 30,
"strategies": [
{"type": "SEMANTIC"},
{"type": "USER_PREFERENCE"}
]
}]
}Memory name rules: alphanumeric + underscores, max 48 chars, starts with a letter.
Env var injected at deploy time: MEMORY_<UPPERCASENAME>_ID Example: memory named UserContext → env var MEMORY_USERCONTEXT_ID
Step 5: Generate wiring code
Read app/<AgentName>/main.py (or the equivalent entrypoint) to detect the framework. Each framework has its own integration pattern — pick the one that matches:
| Framework | Recommended integration | Source |
|---|---|---|
| Strands | AgentCoreMemorySessionManager (CLI template) | bedrock_agentcore.memory.integrations.strands.* |
| LangGraph | AgentCoreMemorySaver + AgentCoreMemoryStore | langgraph-checkpoint-aws (official AWS-maintained) |
| OpenAI Agents SDK | MemoryClient via @function_tool | bedrock_agentcore.memory.MemoryClient |
| Google ADK / Claude Agent SDK | BYO — use MemoryClient directly | Validate end-to-end before shipping |
[!WARNING]
Always check for the MEMORY_ID env var before initializing memory. Memory is NOT
available during agentcore dev — the env var is only set after deploy. Code thatassumes memory is always available will fail silently in local development.
Strands — Session Manager pattern (recommended for new projects)
import os
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig, RetrievalConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager
from strands import Agent
from model.load import load_model # scaffolded by `agentcore create`
app = BedrockAgentCoreApp()
# AgentCore injects this env var after deploy.
# Format: MEMORY_<UPPERCASENAME>_ID
MEMORY_ID = os.getenv("MEMORY_<UPPERCASENAME>_ID")
REGION = os.getenv("AWS_REGION", "us-east-1")
@app.entrypoint
def invoke(payload, context):
actor_id = payload.get("userId", "default-user")
session_id = getattr(context, "session_id", "default-session")
session_manager = None
if MEMORY_ID:
# RetrievalConfig parameters:
# top_k: max number of memory records to retrieve per namespace (SDK default: 10)
# relevance_score: similarity threshold, 0 = return anything, 1 = exact match (SDK default: 0.2)
# The CLI template deviates from SDK defaults to favor precision over recall:
# top_k=3 limits context window usage; relevance_score=0.5 filters low-quality matches.
# Tune these if retrieval misses relevant facts (lower) or surfaces irrelevant ones (raise).
memory_config = AgentCoreMemoryConfig(
memory_id=MEMORY_ID,
session_id=session_id,
actor_id=actor_id,
retrieval_config={
f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5),
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
}
)
session_manager = AgentCoreMemorySessionManager(memory_config, REGION)
agent = Agent(
model=load_model(),
session_manager=session_manager, # None is safe — agent runs without memory
system_prompt="You are a helpful assistant.",
)
result = agent(payload.get("prompt", ""))
return {"response": str(result)}
if __name__ == "__main__":
app.run()Strands — Hook pattern (for adding memory to an existing agent)
import os
from bedrock_agentcore.memory import MemoryClient
from strands.hooks import AgentInitializedEvent, HookProvider, MessageAddedEvent
MEMORY_ID = os.getenv("MEMORY_<UPPERCASENAME>_ID")
memory_client = MemoryClient(region_name=os.getenv("AWS_REGION", "us-east-1")) if MEMORY_ID else None
class MemoryHook(HookProvider):
def on_agent_initialized(self, event):
"""Load recent conversation turns into the agent's context."""
if not MEMORY_ID:
return
session_id = event.agent.state.get("session_id", "default")
turns = memory_client.get_last_k_turns(
memory_id=MEMORY_ID,
actor_id="user",
session_id=session_id,
k=3
)
if turns:
context = "\n".join([
f"{m['role']}: {m['content']['text']}"
for t in turns for m in t
])
event.agent.system_prompt += f"\n\nPrevious conversation:\n{context}"
def on_message_added(self, event):
"""Save each message to memory after it's processed."""
if not MEMORY_ID:
return
session_id = event.agent.state.get("session_id", "default")
msg = event.agent.messages[-1]
memory_client.create_event(
memory_id=MEMORY_ID,
actor_id="user",
session_id=session_id,
messages=[(str(msg["content"]), msg["role"])]
)
def register_hooks(self, registry):
registry.add_callback(AgentInitializedEvent, self.on_agent_initialized)
registry.add_callback(MessageAddedEvent, self.on_message_added)
# Add to your existing agent:
agent = Agent(
# ... your existing config ...
hooks=[MemoryHook()] if MEMORY_ID else [],
state={"session_id": "default"},
)LangGraph — langgraph-checkpoint-aws (recommended)
LangGraph has an official AWS-maintained integration via the `langgraph-checkpoint-aws` package. It provides two integrations that map cleanly to LangGraph's memory model:
- `AgentCoreMemorySaver` — persists LangGraph's checkpoint objects (conversation state, execution graph, metadata) to AgentCore Memory. This is LangGraph's short-term / session memory.
- `AgentCoreMemoryStore` — saves conversational messages for AgentCore's long-term extraction (facts, preferences, summaries) and lets the agent search those memories in future sessions.
Use these instead of wiring MemoryClient calls into your graph manually — they handle the protocol conversion, actor/session mapping, and retry logic for you.
Install:
pip install langgraph-checkpoint-awsRequired IAM permissions on the agent's execution role:
bedrock-agentcore:CreateEventbedrock-agentcore:ListEventsbedrock-agentcore:RetrieveMemories
Basic pattern — short-term checkpointing only:
import os
from langgraph.prebuilt import create_react_agent
from langgraph_checkpoint_aws import AgentCoreMemorySaver
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from model.load import load_model # scaffolded by `agentcore create`
app = BedrockAgentCoreApp()
MEMORY_ID = os.getenv("MEMORY_<UPPERCASENAME>_ID")
REGION = os.getenv("AWS_REGION", "us-east-1")
# Only wire checkpointing if memory is available (deployed)
checkpointer = AgentCoreMemorySaver(MEMORY_ID, region_name=REGION) if MEMORY_ID else None
@app.entrypoint
async def invoke(payload, context):
actor_id = payload.get("userId", "default-user")
session_id = getattr(context, "session_id", "default-session")
graph = create_react_agent(
model=load_model(),
tools=tools,
checkpointer=checkpointer, # None is safe — graph runs without persistence
)
# LangGraph's RunnableConfig maps thread_id → AgentCore session_id,
# actor_id → AgentCore actor_id under the hood
config = {
"configurable": {
"thread_id": session_id,
"actor_id": actor_id,
}
}
result = await graph.ainvoke(
{"messages": [("human", payload["prompt"])]},
config=config,
)
return {"response": result["messages"][-1].content}Full pattern — short-term + long-term retrieval:
For long-term memory (facts, preferences, summaries extracted by AgentCore), add AgentCoreMemoryStore with a pre-model hook that saves messages for extraction and (optionally) retrieves relevant memories:
import os
import uuid
from langchain_core.messages import HumanMessage
from langchain_core.runnables import RunnableConfig
from langgraph.prebuilt import create_react_agent
from langgraph.store.base import BaseStore
from langgraph_checkpoint_aws import AgentCoreMemorySaver, AgentCoreMemoryStore
MEMORY_ID = os.getenv("MEMORY_<UPPERCASENAME>_ID")
REGION = os.getenv("AWS_REGION", "us-east-1")
checkpointer = AgentCoreMemorySaver(MEMORY_ID, region_name=REGION) if MEMORY_ID else None
store = AgentCoreMemoryStore(MEMORY_ID, region_name=REGION) if MEMORY_ID else None
def pre_model_hook(state, config: RunnableConfig, *, store: BaseStore):
"""Save the latest human message for async extraction; optionally retrieve preferences."""
actor_id = config["configurable"]["actor_id"]
thread_id = config["configurable"]["thread_id"]
namespace = (actor_id, thread_id)
messages = state.get("messages", [])
for msg in reversed(messages):
if isinstance(msg, HumanMessage):
store.put(namespace, str(uuid.uuid4()), {"message": msg})
break
# Optional: retrieve user preferences to inject into context
# preferences_ns = ("preferences", actor_id)
# preferences = store.search(preferences_ns, query=msg.content, limit=5)
return {"llm_input_messages": messages}
graph = create_react_agent(
model=load_model(),
tools=tools,
checkpointer=checkpointer,
store=store,
pre_model_hook=pre_model_hook if store else None,
)Invoke with config:
config = {"configurable": {"thread_id": "session-1", "actor_id": "user-alice"}}
response = graph.invoke({"messages": [("human", "I prefer short answers.")]}, config=config)
# New session for the same actor — long-term memories are retrieved
new_config = {"configurable": {"thread_id": "session-2", "actor_id": "user-alice"}}
response = graph.invoke({"messages": [("human", "Summarize my latest report.")]}, config=new_config)The agent remembers "I prefer short answers" across sessions because AgentCore Memory extracts it as a user preference. See the AgentCore docs on LangGraph integration for the full reference.
If you need low-level control (custom retrieval queries, direct event management), fall back to MemoryClient:
from bedrock_agentcore.memory import MemoryClient
client = MemoryClient(region_name=REGION)
# client.create_event(...), client.retrieve_memories(...), client.get_last_k_turns(...)Use MemoryClient directly only when the checkpoint/store abstractions don't fit your use case.
OpenAI Agents SDK — memory as function tools
The OpenAI Agents SDK pattern is to expose memory as @function_tool decorated functions. The agent decides when to read and write:
import os
from agents import Agent, Runner, function_tool
from bedrock_agentcore.memory import MemoryClient
MEMORY_ID = os.getenv("MEMORY_<UPPERCASENAME>_ID")
REGION = os.getenv("AWS_REGION", "us-east-1")
_client = MemoryClient(region_name=REGION) if MEMORY_ID else None
def _build_memory_tools(actor_id: str, session_id: str):
"""Factory — binds actor/session into tool closures."""
@function_tool
def recall_context(query: str, top_k: int = 3) -> str:
"""Search long-term memory for facts or preferences about the user."""
if not _client or not MEMORY_ID:
return "Memory unavailable."
try:
memories = _client.retrieve_memories(
memory_id=MEMORY_ID,
namespace=f"/users/{actor_id}/facts",
query=query,
top_k=top_k,
)
return "\n".join(m.get("content", {}).get("text", "") for m in memories) or "No relevant memories."
except Exception as e:
return f"Memory error: {e}"
@function_tool
def save_fact(content: str) -> str:
"""Save a fact to long-term memory."""
if not _client or not MEMORY_ID:
return "Memory unavailable."
try:
_client.create_event(
memory_id=MEMORY_ID,
actor_id=actor_id,
session_id=session_id,
messages=[(content, "ASSISTANT")],
)
return "Saved."
except Exception as e:
return f"Error: {e}"
return [recall_context, save_fact]
@app.entrypoint
async def invoke(payload, context):
actor_id = payload.get("userId", "default-user")
session_id = getattr(context, "session_id", "default-session")
agent = Agent(
name="Assistant",
instructions="Use recall_context at the start of each session to check what you know about the user. Use save_fact when the user tells you something worth remembering.",
tools=_build_memory_tools(actor_id, session_id),
)
result = await Runner.run(agent, payload["prompt"])
return {"response": result.final_output}Google ADK and Claude Agent SDK — bring your own memory integration
AgentCore Memory doesn't have a framework-specific integration for ADK or the Claude Agent SDK yet, and the samples repo doesn't contain a combined pattern we can point to. Use the general MemoryClient API and wire it into the framework's existing extension points:
- Google ADK: Expose memory operations as ADK tools (functions passed to
Agent(tools=[...])). The ADK agent decides when to call them. - Claude Agent SDK: Wrap
query()with a pre-call memory load and a post-call memory save. The SDK'sClaudeAgentOptions.system_promptis the injection point for retrieved context.
For both frameworks, follow the MemoryClient API shown in the OpenAI Agents pattern above — the client calls (retrieve_memories, create_event, get_last_k_turns) are identical. The framework-specific part is just where you call them.
Before shipping a memory integration for ADK or Claude SDK, validate the end-to-end flow against a deployed agent:
1. Deploy with memory enabled 2. Invoke the agent with facts to remember 3. Start a new session 4. Invoke again and verify the agent recalls those facts 5. Check agentcore logs --runtime <AgentName> --query "memory" --since 1h --level error for any memory errors
If you build a working pattern, consider contributing it to `awslabs/agentcore-samples` so the next developer doesn't have to figure it out.
Step 6: Explain the local dev gap and next steps
Always include this note:
⚠️ Memory is not available during local development (agentcore dev).
The MEMORY_<NAME>_ID env var is only injected after deploy. The code above
handles this gracefully — it runs without memory when the env var isn't set.
To test memory:
agentcore deploy -y
agentcore invoke "My name is Alex and I prefer concise answers"
agentcore invoke "What do you know about me?"
If using long-term memory (SEMANTIC or USER_PREFERENCE), wait 5–30 seconds
between the first and second invoke — extraction runs asynchronously after
each session ends.
Session ID note: use UUIDs (v4) for session IDs — they satisfy the platform's
minimum length requirement (33 characters) and are what `agentcore invoke`
generates by default. Short or sequential session IDs (e.g., "session-1",
"test") can cause long-term memory extraction to fail silently.If the developer is using the SDK directly (no CLI project), they need to create the memory resource first:
from bedrock_agentcore.memory import MemoryClient
client = MemoryClient(region_name="us-east-1")
# Create memory and wait for it to become ACTIVE (takes 2-5 minutes)
memory = client.create_memory_and_wait(
name="UserMemory",
strategies=[
{"userPreferenceMemoryStrategy": {
"name": "prefs",
"namespaces": ["/user/preferences/"]
}},
{"semanticMemoryStrategy": {
"name": "facts",
"namespaces": ["/user/facts/"]
}}
],
event_expiry_days=30
)
MEMORY_ID = memory["id"]
print(f"Memory created: {MEMORY_ID}")
# Set this as an env var or hardcode for testing:
# export MEMORY_ID=<value>Then use the same wiring code from Step 5, reading MEMORY_ID from the environment.
Debugging memory recall
If memory was working and stopped, or never worked:
Agent keeps forgetting things even with memory set up: Most common cause: the memory resource is configured but the code isn't reading from it at session start. Check that your entrypoint calls get_last_k_turns (or uses the session manager) before creating the agent, not after. Also verify the MEMORY_<NAME>_ID env var is set — it's only injected after deploy, not during agentcore dev.
Memory not persisting across sessions:
1. Check that LTM strategies (SEMANTIC, USER_PREFERENCE) are configured — not just SUMMARIZATION 2. Wait 5–30 seconds after a session ends before starting a new one — extraction is async 3. Verify the memory resource is ACTIVE: agentcore status --type memory 4. Use UUIDs (v4) for session IDs — the platform requires a minimum of 33 characters. Short IDs like "session-1" or "test" cause LTM to fail silently. agentcore invoke generates compliant IDs by default.
Memory not loading at session start:
1. Verify MEMORY_<NAME>_ID env var is set: agentcore status --type memory --json 2. Check the actor_id is consistent across sessions — memory is scoped per actor 3. Confirm the namespace paths in retrieval_config match the namespaces used when writing — the retrieval namespace must exactly match the namespace the strategy extracts into 4. CLI defaults use paths without trailing slashes (e.g., /users/{actorId}/facts). If you customized namespace templates when creating the memory resource, use whatever pattern you chose — consistency between writer and reader is what matters.
Memory provisioning slow: Memory takes 2–5 minutes to become ACTIVE after agentcore deploy. Check status:
agentcore status --type memoryS3 delivery / export buckets must be in the same account
If you're configuring S3 delivery for memory exports, session transcripts, or Browser recording output, the destination bucket must be in the same AWS account as the AgentCore resource. Cross-account S3 buckets are not supported as delivery destinations, even with correct bucket policies granting the service principal access.
Symptom of attempting a cross-account bucket: CreateMemory (or the relevant resource creation call) fails with ValidationException: Role does not have access to required S3 buckets — even when IAM and bucket policies are correctly configured for cross-account access.
Workaround: create a same-account bucket for the AgentCore resource to write to. If you need the data in a different account, replicate from the same-account bucket via S3 replication or a scheduled copy job.
Sharing memory across agents
Memory is a top-level resource — not nested under a single agent. To share:
1. Create one memory resource: agentcore add memory --name SharedMemory --strategies SEMANTIC 2. In each agent's code, read the same env var: MEMORY_SHAREDMEMORY_ID 3. Use a consistent actor_id scheme across agents (e.g., the end user's ID)
Cross-region inference (data residency)
Memory consolidation (extraction + summarization for long-term strategies) uses cross-region inference by default. Your memory data stays in your primary region, but the inference call that extracts facts or summarizes a session may execute in another AWS region within the same geography (e.g., us-east-1 → us-east-2 or us-west-2; EU stays in EU; etc.).
This matters when:
- You have a data-residency requirement that goes beyond storage — some regulations constrain where inference may run, not just where results land
- You're building for a customer whose contract pins processing to a single region
- Your audit trail needs to show which region handled each prompt
There's no extra cost for cross-region inference, and CloudWatch/CloudTrail logs don't include the inference region. Across the Memory, Policy, and Evaluations services, this is the default behavior.
To opt out for Memory: use a built-in-with-overrides strategy (see `memory-custom-strategy`) and pin the model to a specific region. The overrides strategy lets you specify the exact model ID used for extraction and consolidation, which gives you region control.
The supported geographies and inference-region mappings change as AgentCore expands — check the cross-region inference docs for the current list rather than baking it in here.
Beyond the CLI: memory features that require the API
The CLI's agentcore add memory and agentcore.json cover strategy selection, expiry, and basic configuration. Some memory capabilities are API/SDK-only — the CLI doesn't expose them. When the developer needs one of these, the graduation path is: create the memory via CLI as usual, deploy, then apply the additional config via boto3 or AWS CLI.
Resource-based policies (cross-account access, principal-level restrictions):
import boto3, json
client = boto3.client("bedrock-agentcore-control")
memory_id = "<MEMORY_ID>" # from: agentcore status --type memory --json
client.put_memory_resource_policy(
memoryId=memory_id,
policy=json.dumps({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111122223333:root"},
"Action": [
"bedrock-agentcore:CreateEvent",
"bedrock-agentcore:RetrieveMemories",
"bedrock-agentcore:ListEvents"
],
"Resource": "*"
}]
})
)Custom extraction models (pin the model used for LTM extraction — e.g., for data residency):
Use the "built-in with overrides" strategy type via UpdateMemory. See the custom memory strategy docs for the full configuration shape.
Self-managed strategies (bring your own extraction logic):
Also API-only. See the AgentCore memory docs for the selfManagedMemoryStrategy configuration.
When you hit a memory capability not covered here, use the awsknowledge MCP server if available — search for the specific API operation (e.g., "AgentCore PutMemoryResourcePolicy") to get the current parameter shapes. The API surface evolves between releases.
General rule: if agentcore.json has a field for it, use the CLI. If it doesn't, create the resource via CLI, deploy, then apply the additional config via boto3. Don't fight the CLI to do something it wasn't designed for.
Output
- Updated
agentcore/agentcore.jsonwith memory resource (via CLI command) - Wiring code for
app/<AgentName>/main.pyappropriate for the detected framework - Explanation of the local dev gap and how to test after deploy
Quality criteria
- Generated code handles
MEMORY_IDbeing None (local dev) without crashing - Env var name matches the memory resource name in
agentcore.json(uppercase, underscores) - Framework-specific pattern is used — never generate Strands hooks for a LangGraph project
- LTM extraction delay is communicated
- Session ID guidance recommends UUIDs (v4) when LTM strategies are used (minimum 33 characters)
migrate
Move an existing Amazon Bedrock Agent to AgentCore Runtime.
When to use
- You have an existing Bedrock Agent (created via the Bedrock console or API) and want to run it on AgentCore Runtime
- You want to add AgentCore capabilities (Memory, Gateway, Observability) to an existing agent
- You want to move from the declarative Bedrock Agents model to a code-first framework
Input
$ARGUMENTS is optional:
/migrate # interactive — walks through the migration
/migrate strands # migrate targeting Strands framework
/migrate langgraph # migrate targeting LangGraph frameworkWhat migration does
The agentcore create --type import command reads your existing Bedrock Agent's configuration and generates an AgentCore project that reproduces its behavior in a code-first framework. Specifically:
- System prompt → copied into the generated
main.py - Action groups (Lambda-backed) → converted to Gateway targets with
--type lambda-function-arn - Knowledge bases → referenced in the system prompt with a note to wire retrieval manually (AgentCore doesn't auto-import KB bindings)
- Guardrails → noted in comments but not auto-converted (AgentCore uses Cedar policies, not Bedrock Guardrails)
- Agent alias / version → the import targets a specific alias, not the draft
What migration does not do:
- It does not delete or modify the original Bedrock Agent — the source agent keeps running
- It does not migrate conversation history or session state
- It does not convert Bedrock Guardrails to Cedar policies (different authorization model)
- It does not auto-wire Knowledge Base retrieval — you'll need to add that as a tool or direct SDK call
Prerequisites
1. The Bedrock Agent must exist and have at least one alias 2. Your AWS credentials must have bedrock:GetAgent, bedrock:GetAgentAlias, and bedrock:ListAgentActionGroups permissions 3. You need the agent ID, alias ID, and region
Process
Step 1: Run the import
agentcore create \
--type import \
--agent-id <AGENT_ID> \
--agent-alias-id <ALIAS_ID> \
--region <REGION> \
--name <ProjectName> \
--framework StrandsThe --framework flag determines which code-first framework the generated project uses. Strands is recommended for the closest mapping to Bedrock Agent behavior.
Project name rules apply: max 23 characters, alphanumeric only, starts with a letter.
Step 2: Review the generated project
cd <ProjectName>
cat app/<AgentName>/main.py
cat agentcore/agentcore.jsonCheck:
- The system prompt matches your original agent's instructions
- Action groups appear as Gateway targets in
agentcore.json(underagentCoreGateways) - The model ID is correct for your target region
Step 3: Fill in what migration doesn't cover
Knowledge Bases: If your Bedrock Agent used Knowledge Bases, you have two options:
1. Keep using the KB via boto3 — call bedrock-agent-runtime:RetrieveAndGenerate or Retrieve directly from your agent code as a tool 2. Replace with AgentCore Memory — if the KB was used for user-specific context, AgentCore Memory with SEMANTIC strategy may be a better fit. See memory.md.
Guardrails → Cedar policies: Bedrock Guardrails (content filters, denied topics, word filters) don't have a 1:1 mapping to Cedar policies. Cedar policies control which tools the agent can call and with what parameters — they're authorization rules, not content filters. If you need content filtering, keep the guardrail logic in your agent code (pre/post-processing) or use Bedrock Guardrails as a standalone API call.
Custom orchestration: If your Bedrock Agent used custom orchestration (return-of-control, custom Lambda orchestrators), you'll need to rebuild that logic in the framework's native patterns — Strands tool chains, LangGraph graph nodes, etc.
Step 4: Test locally and deploy
# Test locally (memory and gateway won't be available yet)
agentcore dev
# Deploy when ready
agentcore deploy -y
# Verify
agentcore invoke "Hello, what can you do?"
agentcore statusStep 5: Cut over traffic
Once the AgentCore agent is working correctly:
1. Update your application to invoke the AgentCore Runtime instead of the Bedrock Agent 2. See integrate.md for the invocation patterns (SigV4, JWT, SDK) 3. Keep the original Bedrock Agent running as a fallback until you're confident 4. Delete the Bedrock Agent only after the AgentCore agent has been stable in production
Common migration issues
"Model not available in target region" The imported agent may reference a model ID that isn't available in your AgentCore deployment region. Edit model/load.py to use a cross-region inference profile or a model available in your region.
"Action group Lambda in a different region" Gateway targets can invoke Lambda functions cross-region, but latency increases. Consider deploying the Lambda in the same region as your AgentCore agent, or accept the latency trade-off.
"Agent behavior differs after migration" The most common cause is prompt format differences between Bedrock Agent's orchestration and the code-first framework. Bedrock Agent injects structured XML around tool results; Strands/LangGraph use different formats. Tune the system prompt to compensate.
Output
- A working AgentCore project that reproduces the Bedrock Agent's behavior
- A list of what was auto-converted and what needs manual work
- Guidance on cutting over traffic from the old agent to the new one
multi-agent
Build AgentCore systems where agents delegate work to other agents.
When to use
- You want an orchestrator agent to delegate complex tasks to a specialist
- You're building a system where agents have different roles and capabilities
- You want agents to discover and communicate with each other via the A2A standard
- You want multiple agents to share the same memory
Input
$ARGUMENTS is optional:
/multi-agent # interactive — asks which pattern you need
/multi-agent a2a # A2A protocol setup
/multi-agent direct # direct invocation pattern
/multi-agent memory # shared memory across agentsChoosing a pattern
Step 1: Deploy the specialist agent
The specialist is a standard AgentCore agent. Deploy it normally:
agentcore create --name SpecialistAgent --defaults
# ... add your specialist logic to app/SpecialistAgent/main.py ...
agentcore deploy -yGet the specialist's runtime ARN after deploy:
agentcore status --runtime SpecialistAgent --json | jq -r '.runtimes[0].arn'Step 2: Add the specialist as a tool in the orchestrator
The orchestrator calls the specialist via bedrock-agentcore:InvokeAgentRuntime. Add this tool to your orchestrator's agent code:
import os
import json
import boto3
from bedrock_agentcore.runtime import BedrockAgentCoreApp
app = BedrockAgentCoreApp()
# Set this env var in your orchestrator's deployment config
SPECIALIST_ARN = os.getenv("SPECIALIST_AGENT_ARN")
REGION = os.getenv("AWS_REGION", "us-east-1")
def call_specialist(prompt: str, session_id: str = None) -> str:
"""
Call the specialist agent and return its response.
The specialist runs in its own isolated session.
"""
client = boto3.client("bedrock-agentcore", region_name=REGION)
kwargs = {
"agentRuntimeArn": SPECIALIST_ARN,
"qualifier": "DEFAULT", # or a specific version number to pin
"payload": json.dumps({"prompt": prompt}).encode(),
}
if session_id:
kwargs["runtimeSessionId"] = session_id
response = client.invoke_agent_runtime(**kwargs)
# response["response"] is a StreamingBody — read, then parse JSON
body = response["response"].read()
result = json.loads(body.decode() if isinstance(body, bytes) else body)
return result.get("response", result.get("result", str(result)))Passing "DEFAULT" as the qualifier calls the live version. To pin to a specific version (staging pin, canary, or rollback), pass a numeric version string instead — see `agents-deploy/references/versioning.md` for the full workflow.
For Strands, register it as a @tool:
from strands import Agent, tool
@tool
def delegate_to_specialist(task: str) -> str:
"""
Delegate a complex analysis task to the specialist agent.
Use this when the task requires deep domain expertise.
Args:
task: The specific task or question for the specialist.
Returns:
The specialist's detailed response.
"""
return call_specialist(task)
@app.entrypoint
def invoke(payload, context):
agent = Agent(
model=load_model(), # scaffolded by `agentcore create`
system_prompt="""You are an orchestrator. For complex analysis tasks,
delegate to the specialist using the delegate_to_specialist tool.
Synthesize the specialist's response for the user.""",
tools=[delegate_to_specialist],
)
result = agent(payload.get("prompt", ""))
return {"response": str(result)}
if __name__ == "__main__":
app.run()For LangGraph, add it as a tool node:
from langchain_core.tools import tool as lc_tool
@lc_tool
def delegate_to_specialist(task: str) -> str:
"""Delegate complex tasks to the specialist agent."""
return call_specialist(task)
# Add to your LangGraph tool node
tools = [delegate_to_specialist]
tool_node = ToolNode(tools)
llm_with_tools = llm.bind_tools(tools)For OpenAI Agents SDK, register as a @function_tool:
from agents import Agent, Runner, function_tool
@function_tool
def delegate_to_specialist(task: str) -> str:
"""Delegate a complex analysis task to the specialist agent.
Use when the task requires deep domain expertise."""
return call_specialist(task)
@app.entrypoint
async def invoke(payload, context):
agent = Agent(
name="Orchestrator",
instructions="For complex analysis, delegate to the specialist using delegate_to_specialist. Synthesize the response for the user.",
tools=[delegate_to_specialist],
)
result = await Runner.run(agent, payload["prompt"])
return {"response": result.final_output}For Google ADK, pass as a plain function in the agent's tools=[] list. Note: the official samples use A2A for ADK multi-agent patterns (see awslabs/agentcore-samples/02-use-cases/A2A-multi-agent-incident-response/host_adk_agent/). The direct-invocation pattern below is extrapolated from the ADK base template — validate against your ADK version before relying on it in production:
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
def delegate_to_specialist(task: str) -> str:
"""Delegate complex analysis to the specialist agent."""
return call_specialist(task)
agent = Agent(
model="gemini-2.5-flash",
name="orchestrator",
description="Orchestrator that delegates complex tasks to specialists.",
instruction="For complex analysis, call delegate_to_specialist and synthesize the response.",
tools=[delegate_to_specialist],
)
@app.entrypoint
async def invoke(payload, context):
user_id = payload.get("user_id", "default_user")
session_id = getattr(context, "session_id", "default_session")
session_service = InMemorySessionService()
session = await session_service.create_session(
app_name="orchestrator", user_id=user_id, session_id=session_id
)
runner = Runner(agent=agent, app_name="orchestrator", session_service=session_service)
content = types.Content(role="user", parts=[types.Part(text=payload["prompt"])])
async for event in runner.run_async(user_id=user_id, session_id=session.id, new_message=content):
if event.is_final_response():
return {"response": event.content.parts[0].text}For a validated ADK multi-agent pattern, use A2A instead of direct invocation — see the A2A section below and the sample linked above.
For Claude Agent SDK: See `awslabs/agentcore-samples/03-integrations/agentic-frameworks/claude-agent/claude-sub-agents/` for the official sub-agent pattern. This plugin doesn't ship a Claude SDK delegation pattern because the sample is more current than anything we could extrapolate.
Step 3: Grant IAM permission
The orchestrator's execution role needs permission to invoke the specialist:
{
"Effect": "Allow",
"Action": "bedrock-agentcore:InvokeAgentRuntime",
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<YOUR_ACCOUNT_ID>:runtime/SpecialistAgent-*"
}Add this to agentcore/agentcore.json under the orchestrator agent's IAM config, or add it manually to the auto-created execution role after deploy.
Step 4: Pass the specialist ARN at deploy time
Add the specialist ARN as an environment variable in the orchestrator's deployment:
# After deploying the specialist, get its ARN:
SPECIALIST_ARN=$(agentcore status --runtime SpecialistAgent --json | jq -r '.runtimes[0].arn')
# For local dev, write to .env.local:
echo "SPECIALIST_AGENT_ARN=$SPECIALIST_ARN" >> agentcore/.env.localFor the deployed orchestrator, the specialist ARN needs to be available as an environment variable. The recommended pattern is:
1. Edit `agentcore/agentcore.json` — find the orchestrator agent's entry and add the env var to its configuration (the exact field name depends on your CLI version; run agentcore validate after editing). In current CLI versions, agent environment variables are typically managed through the deployment config.
2. Or use CDK overrides — for teams using the CDK constructs directly, set the env var in the Runtime construct's environment property.
3. Or write the env var at deploy time — some teams use a pre-deploy script that generates agentcore/.env.local and agentcore/agentcore.json updates together:
# pre-deploy.sh — run before every orchestrator deploy
SPECIALIST_ARN=$(agentcore status --runtime SpecialistAgent --json | jq -r '.runtimes[0].arn')
echo "SPECIALIST_AGENT_ARN=$SPECIALIST_ARN" >> agentcore/.env.local
# Then deploy
agentcore deploy -yThe CLI does not currently provide a dedicated --env flag on agentcore add agent. Check agentcore add agent --help for the current options in your CLI version.
---
Pattern 2: A2A protocol
The specialist exposes the A2A standard — discoverable via an agent card, callable via JSON-RPC. AgentCore's A2A runtime handles the HTTP server, port binding (9000), and agent card serving for you.
Step 1: Build the A2A specialist
Use the serve_a2a helper from bedrock-agentcore — this matches what the CLI scaffolds via agentcore create --protocol A2A.
# app/SpecialistA2A/main.py
from strands import Agent, tool
from strands.multiagent.a2a.executor import StrandsA2AExecutor
from bedrock_agentcore.runtime import serve_a2a
from model.load import load_model
@tool
def analyze_data(dataset_name: str) -> str:
"""Run detailed analysis on the named dataset."""
# Your specialist logic here
return f"Analysis results for {dataset_name}..."
agent = Agent(
model=load_model(),
system_prompt="You are an analysis specialist. Use tools when appropriate.",
tools=[analyze_data],
)
if __name__ == "__main__":
serve_a2a(StrandsA2AExecutor(agent))# requirements.txt
strands-agents[a2a]
bedrock-agentcoreserve_a2a handles port 9000 binding, agent card generation at /.well-known/agent-card.json, and JSON-RPC routing automatically. No FastAPI or uvicorn needed.
Step 2: Deploy the A2A specialist
agentcore create --name SpecialistA2A --protocol A2A
# The CLI scaffolds app/SpecialistA2A/main.py with the serve_a2a pattern shown above — customize it with your specialist logic
agentcore deploy -yAfter deploy, get the runtime URL:
agentcore fetch access --name SpecialistA2A --type agentStep 3: Test locally
# Start the A2A server locally (from your project dir)
agentcore dev
# Test the agent card (discovery)
curl http://localhost:9000/.well-known/agent-card.json | jq .
# Send a message
curl -X POST http://localhost:9000 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-001",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "What is 42 * 17?"}],
"messageId": "msg-001"
}
}
}' | jq .Step 4: Call the A2A specialist from the orchestrator
The specialist URL is a non-secret identifier, so pass it via an env var in the orchestrator's deployment config. The bearer token is a secret — do not stash it in os.getenv(...) on the deployed runtime (runtime env vars are not vault-backed). Register an OAuth M2M provider once, then use @requires_access_token to fetch a fresh token at call time:
# One-time: register the OAuth provider that issues tokens for the specialist.
# Omit --client-secret to get an interactive prompt (value goes straight into the credential provider).
agentcore add credential \
--name SpecialistA2A \
--type oauth \
--discovery-url https://<YOUR_IDP>/.well-known/openid-configuration \
--client-id <CLIENT_ID> \
--scopes a2a.invokeimport asyncio
import os
from uuid import uuid4
import httpx
from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
from a2a.types import Message, Part, Role, TextPart
from bedrock_agentcore.identity.auth import requires_access_token
# Non-secret identifier — fine to pull from the environment.
SPECIALIST_URL = os.getenv("SPECIALIST_A2A_URL")
@requires_access_token(
provider_name="SpecialistA2A",
scopes=["a2a.invoke"],
auth_flow="M2M",
)
async def call_a2a_specialist(message: str, *, access_token: str) -> str:
session_id = str(uuid4())
headers = {
"Authorization": f"Bearer {access_token}",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": session_id,
}
async with httpx.AsyncClient(timeout=300, headers=headers) as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=SPECIALIST_URL)
agent_card = await resolver.get_agent_card()
config = ClientConfig(httpx_client=http_client, streaming=False)
client = ClientFactory(config).create(agent_card)
msg = Message(
kind="message",
role=Role.user,
parts=[Part(TextPart(kind="text", text=message))],
message_id=uuid4().hex,
)
async for event in client.send_message(msg):
if hasattr(event, "parts"):
return " ".join(p.text for p in event.parts if hasattr(p, "text"))
return ""
# Use in your orchestrator's entrypoint:
@app.entrypoint
def invoke(payload, context):
result = asyncio.run(call_a2a_specialist(payload.get("prompt", "")))
return {"response": result}The decorator handles caching and refresh. For local dev, put the OAuth values in agentcore/.env.local so agentcore dev can resolve the decorator — the deployed runtime reads them from the credential provider instead.
---
Shared memory across agents
Memory is a top-level resource — not nested under a single agent. Multiple agents can share it by reading the same env var.
Setup
1. Create one shared memory resource:
agentcore add memory --name SharedMemory --strategies SEMANTIC,USER_PREFERENCE1. In each agent's code, read the same env var:
MEMORY_ID = os.getenv("MEMORY_SHAREDMEMORY_ID")1. Use a consistent actor_id scheme — typically the end user's ID — so both agents read and write the same user's memory.
Key consideration
When multiple agents share memory, they share the same namespace. Use namespaced paths to avoid collisions:
# Orchestrator writes to /orchestrator/ namespace
memory_client.create_event(
memory_id=MEMORY_ID,
actor_id=user_id,
session_id=session_id,
messages=[("User asked about X", "user")],
)
# Specialist reads from all namespaces
turns = memory_client.get_last_k_turns(
memory_id=MEMORY_ID,
actor_id=user_id,
session_id=session_id,
k=5,
)---
Troubleshooting
A2A server not responding:
- Verify it's running on port 9000 (not 8080)
- Check the agent card endpoint returns:
curl http://localhost:9000/.well-known/agent-card.json - Verify your
main.pyusesserve_a2a(StrandsA2AExecutor(agent))— the olderA2AServer + FastAPIpattern is deprecated in favor of this
Direct invocation permission denied:
- Check the orchestrator's execution role has
bedrock-agentcore:InvokeAgentRuntime - Verify the resource ARN pattern matches the specialist's ARN
- IAM changes take ~30 seconds to propagate
Specialist not found:
- Verify
SPECIALIST_AGENT_ARNenv var is set correctly - Check
agentcore status --runtime SpecialistAgentshowsdeployedstate
A2A auth errors:
- A2A supports SigV4 and OAuth 2.0 — make sure you're using the right auth method
- Get the correct bearer token:
agentcore fetch access --name SpecialistA2A --type agent
Output
- Decision tree to choose the right pattern
- Complete code for the chosen pattern (orchestrator + specialist)
- IAM policy for agent-to-agent invocation
- Local testing commands
Quality criteria
- Pattern recommendation matches the developer's latency and interoperability needs
- Generated code includes correct IAM permissions for agent-to-agent invocation
- A2A server runs on port 9000 (not 8080) using
serve_a2a(StrandsA2AExecutor(agent)) - Agent card is at
/.well-known/agent-card.jsonwith correct capabilities - Shared memory uses consistent
actor_idscheme across agents
request-headers
Pass custom HTTP headers from the caller through to your agent's invocation code.
When to use
- You need to pass a tenant ID, correlation ID, or feature flag from your app to your agent
- You're implementing a protocol that requires specific headers (A2A, MCP, vendor-specific)
- You want OpenTelemetry baggage or trace headers to propagate from the caller
- You tried adding a header to the request and your agent code never sees it
- You're integrating with an external system that uses idempotency keys or similar headers
The default: most headers are stripped
AgentCore Runtime strips all incoming headers from the request before it reaches your agent code except:
Authorization— always passed through- Any header matching
X-Amzn-Bedrock-AgentCore-Runtime-Custom-*— this is the reserved prefix for custom headers
Anything else — X-Tenant-Id, X-Correlation-Id, traceparent, A2A-Version, Idempotency-Key, whatever — will not appear in your invocation context unless you explicitly add it to the runtime's request header allowlist.
This is an intentional security boundary: the runtime doesn't forward arbitrary caller-supplied headers by default. It's also the #1 reason developers ask "why can't my agent see the header I'm sending?"
Two ways to pass custom data
Option 1: Use the reserved prefix
Rename headers at the caller to use the X-Amzn-Bedrock-AgentCore-Runtime-Custom- prefix. These pass through without any runtime configuration change.
# Caller sends:
X-Amzn-Bedrock-AgentCore-Runtime-Custom-Tenant-Id: acme-corp
X-Amzn-Bedrock-AgentCore-Runtime-Custom-Correlation-Id: 8b2e3d...
# Agent code sees the same headers in the invocation contextThis is the simplest option for headers you control end-to-end (your app, your agent).
Option 2: Add headers to the request header allowlist
If the header names are fixed by a protocol or external system (A2A requires A2A-Version and A2A-Extensions; OpenTelemetry uses traceparent and baggage; some APIs use Idempotency-Key), you can't rename them. Configure the runtime to allow them explicitly.
Edit `agentcore/agentcore.json` and add requestHeaderAllowlist to the runtime entry:
{
"runtimes": [
{
"name": "MyAgent",
"requestHeaderAllowlist": [
"X-Amzn-Bedrock-AgentCore-Runtime-Custom-X-Tenant-Id",
"X-Amzn-Bedrock-AgentCore-Runtime-Custom-A2A-Version"
]
}
]
}Then agentcore deploy. The $schema URL at the top of the file (https://schema.agentcore.aws.dev/v1/agentcore.json) gives IDE autocomplete and validation for every field.
CLI shortcut — agentcore add agent --request-header-allowlist "X-Tenant-Id,A2A-Version" writes the same array. Important: the CLI auto-prefixes entries with X-Amzn-Bedrock-AgentCore-Runtime-Custom- as they land in agentcore.json. If you're editing the JSON by hand, write the prefixed form directly. If you're using the CLI, pass the short name and let the CLI add the prefix.
Authorization passes through by default and doesn't need to be in the allowlist.
Constraints
- Maximum 20 headers in the allowlist (including
Authorizationif you include it explicitly) - Header name length: up to 256 characters
- Header value size: up to 4 KB per header
- Names are case-sensitive — list them exactly as they'll be sent
- Changes take effect after the next deploy of the runtime
If you hit the 20-header cap, combine related data into one JSON-encoded header rather than using many separate ones.
Common use cases
Multi-tenancy
Caller: X-Tenant-Id: acme-corp
Agent code: reads tenant from the header, scopes memory/data/tools per tenantAdd X-Tenant-Id to the allowlist. The agent can then isolate memory namespaces, database queries, and tool-call authorization per tenant.
Distributed tracing propagation
Caller: traceparent: 00-<trace-id>-<span-id>-01
baggage: userId=alice,env=prod
Agent code: uses OTel SDK to continue the parent traceAdd traceparent and baggage to the allowlist. Your OTel SDK instrumentation will pick them up automatically and produce spans connected to the caller's trace.
A2A protocol compliance
Caller: A2A-Version: 1.0
A2A-Extensions: x-capability-foo
Agent code: branches behavior based on protocol versionA2A v1.0 requires these headers. Add both to the allowlist; A2A v0.3 doesn't need either.
Idempotency keys
Caller: Idempotency-Key: 7f3a...
Agent code: deduplicates or caches based on the keyFor agents that call external APIs with idempotency, propagating the caller's key through to the agent's outbound calls avoids duplicate side effects on retry.
Reading the headers in agent code
Headers arrive in the runtime's context object passed to your invocation handler. The exact accessor depends on the framework — check the bedrock-agentcore SDK docs for your language. In Python:
@app.entrypoint
def invoke(payload, context):
tenant = context.headers.get("X-Tenant-Id")
correlation_id = context.headers.get("X-Correlation-Id")
# ... use as neededHeaders that weren't in the allowlist will be absent (not empty string) from the context.
What won't work
- Sending headers without configuring the allowlist — anything outside the default pass-through set is silently dropped. Your agent code won't see the header, and there's no error. Check the runtime's
requestHeaderConfigurationif a header you expect to see isn't arriving. - Using this for secrets — 4 KB values and the allowlist configuration are designed for metadata, not credentials. Use the AgentCore Identity credential provider for API keys, OAuth tokens, and secrets. See
agents-connectPath D. - Dynamic headers — the allowlist is static runtime configuration. You can't vary it per-request.
Troubleshooting
"My agent doesn't see the header I'm sending" Check (in order): (1) Is the header in the allowlist? (2) Is the spelling an exact match including case? (3) Did you redeploy the runtime after updating the allowlist? (4) Is the caller actually sending the header — curl -v or equivalent network inspection.
"I hit the 20-header limit" Consolidate related data into a single JSON-encoded header. For example, instead of X-Region, X-Environment, X-Service-Name as three separate headers, use X-Context: {"region":"us-west-2","env":"prod","service":"billing"}.
"Allowlist update didn't take effect" Redeploy the runtime. The header allowlist is config that applies on the next agentcore deploy, not immediately after editing agentcore.json.
Output
- Decision on prefix vs. allowlist approach
- CLI command to update the allowlist if needed
- Agent code pattern for reading the headers
teardown
Remove individual resources from your project or tear down the entire deployment.
When to use
- You want to remove a gateway, memory, credential, evaluator, or other resource from your project
- You want to delete a deployed agent and clean up all AWS resources
- You're iterating in a sandbox account and want to start fresh
- You need to remove a resource that's stuck or no longer needed
Process
Removing individual resources from your project
Use agentcore remove to remove a resource from agentcore.json. This marks the resource for deletion — the actual AWS resource is removed on the next agentcore deploy.
# Remove a memory resource
agentcore remove memory --name MyMemory
# Remove a gateway target
agentcore remove gateway-target --name WeatherTools --gateway MyGateway
# Remove a gateway (remove all its targets first)
agentcore remove gateway --name MyGateway
# Remove a credential
agentcore remove credential --name MyAPIKey
# Remove an evaluator
agentcore remove evaluator --name ResponseQuality
# Remove an online eval config
agentcore remove online-eval --name production_monitor
# Remove a policy
agentcore remove policy --name SpendingLimit --engine MyPolicyEngine
# Remove a policy engine (remove all its policies first)
agentcore remove policy-engine --name MyPolicyEngineAfter removing, deploy to apply the changes:
agentcore deploy -yCheck what's pending removal before deploying:
agentcore status --state pending-removalRemoving an agent from a multi-agent project
If your project has multiple agents (runtimes), you can remove one:
agentcore remove agent --name SecondAgent
agentcore deploy -yThis deletes the agent's runtime, endpoint, and associated resources from AWS. The agent's code in app/<AgentName>/ is not deleted — remove it manually if you no longer need it.
Tearing down the entire deployment
To remove all deployed AWS resources for a project:
# Preview what will be destroyed
agentcore deploy --diff
# Destroy all resources
npx cdk destroy --app "npx ts-node agentcore/cdk/bin/cdk.ts" --forceAlternatively, delete the CloudFormation stack directly:
# Find the stack name
aws cloudformation list-stacks \
--stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE \
--query "StackSummaries[?contains(StackName, '<ProjectName>')].StackName"
# Delete it
aws cloudformation delete-stack --stack-name <StackName>
# Wait for deletion to complete
aws cloudformation wait stack-delete-complete --stack-name <StackName>What gets deleted and what doesn't
| Resource | Deleted by cdk destroy | Notes |
|---|---|---|
| AgentCore Runtime(s) | ✅ | Includes all endpoints and versions |
| Memory resource(s) | ✅ | Memory data is deleted permanently |
| Gateway(s) and targets | ✅ | |
| Credentials | ✅ | Secrets Manager entries are removed |
| Policy engine(s) and policies | ✅ | |
| Evaluator definitions | ✅ | |
| Online eval configs | ✅ | |
| IAM roles | ✅ | Created by CDK |
| CloudWatch log groups | ❌ | Persist after deletion — delete manually if needed |
| ECR images (Container builds) | ❌ | Persist — delete the repository manually |
| CDK bootstrap stack | ❌ | Shared across projects — don't delete unless you're done with CDK entirely |
| Local project files | ❌ | agentcore/, app/ — delete manually |
Cleaning up CloudWatch log groups
Log groups persist after stack deletion. To clean them up:
# List AgentCore log groups
aws logs describe-log-groups \
--log-group-name-prefix /aws/bedrock-agentcore/ \
--query "logGroups[].logGroupName"
# Delete a specific log group
aws logs delete-log-group --log-group-name /aws/bedrock-agentcore/runtimes/<AGENT_ID>-DEFAULTCleaning up ECR repositories (Container builds)
# List AgentCore ECR repositories
aws ecr describe-repositories \
--query "repositories[?contains(repositoryName, 'bedrock-agentcore')].repositoryName"
# Delete a repository and all its images
aws ecr delete-repository --repository-name <repo-name> --forceHandling stuck resources
If a runtime is stuck in DELETING state for more than 30 minutes, see the "Runtime stuck in DELETING" section in agents-debug. The short version: don't keep retrying — open an AWS Support case with the runtime ARN and the original delete request ID from CloudTrail.
Common issues
"Can't remove gateway — targets still attached" Remove all gateway targets first, then remove the gateway:
agentcore remove gateway-target --name Target1 --gateway MyGateway
agentcore remove gateway-target --name Target2 --gateway MyGateway
agentcore remove gateway --name MyGateway"Can't remove policy engine — policies still attached" Remove all policies first, then remove the engine:
agentcore remove policy --name Policy1 --engine MyEngine
agentcore remove policy-engine --name MyEngine"Resource shows pending-removal but deploy doesn't delete it" Check agentcore status --state pending-removal and verify the resource is listed. If deploy completes without removing it, check the CDK output for errors — the deletion may have failed silently due to a dependency.
Output
- CLI commands to remove the specific resource(s)
- Guidance on what persists after deletion and how to clean it up
- Warnings about irreversible data loss (memory data, credentials)
vpc
Configure your AgentCore agent to connect to private AWS resources inside a VPC.
When to use
- Your agent needs to connect to an RDS database
- Your agent needs to call internal APIs not exposed to the internet
- You want to keep your agent's network traffic private
- VPC connectivity is configured but connections are timing out
Input
$ARGUMENTS is optional:
/vpc # interactive — asks what you're connecting to
/vpc rds # RDS database connectivity
/vpc debug # diagnose VPC connectivity issuesHow AgentCore VPC connectivity works
When you configure VPC mode, AgentCore creates Elastic Network Interfaces (ENIs) in your VPC subnets. These ENIs give your agent a private IP address in your VPC, enabling it to reach private resources.
Key facts:
- VPC connectivity directly affects outbound traffic — ENIs route your agent's outbound calls through your VPC. For inbound traffic, you can optionally add an AgentCore VPC endpoint to keep API calls private via PrivateLink (this is separate from the
networkModesetting). - AgentCore creates ENIs via the service-linked role
AWSServiceRoleForBedrockAgentCoreNetwork(auto-created on first VPC deployment) - Subnets must be in supported Availability Zones — not all AZs are supported. The supported AZ list changes as AgentCore expands to new regions.
---
Step 0: Verify CLI version
Run agentcore --version. This skill requires v0.9.0 or later. If the version is older, tell the developer to run agentcore update before proceeding.
---
Step 1: Verify your subnets are in supported AZs
AgentCore only supports specific Availability Zone IDs per region. The supported AZ list changes as AgentCore expands — always check the current docs for the latest table.
Check your subnet's AZ ID:
# Check the AZ ID of your subnet
aws ec2 describe-subnets \
--subnet-ids subnet-12345678 \
--query 'Subnets[0].{AZ:AvailabilityZone,AZId:AvailabilityZoneId,SubnetId:SubnetId}'To find the current supported AZ IDs: See the AgentCore VPC configuration guide: https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-vpc.html — look for the "Supported Availability Zones" section. The table lists AZ IDs (e.g., use1-az1, usw2-az2) per region — use AZ IDs, not AZ names, because AZ name-to-ID mappings differ across AWS accounts.
If your subnet is in an unsupported AZ, the deployment will fail. Use subnets in supported AZs.
Best practice: Use at least two subnets in different supported AZs for high availability.
---
Step 2: Configure security groups
Security groups control what your agent can connect to. Configure them based on what you're connecting to.
Connecting to RDS PostgreSQL
AgentCore agent security group (outbound rule):
Type: Custom TCP
Port: 5432
Destination: RDS security group ID (not CIDR)RDS security group (inbound rule):
Type: PostgreSQL
Port: 5432
Source: AgentCore agent security group ID# Create a security group for the agent
aws ec2 create-security-group \
--group-name agentcore-agent-sg \
--description "AgentCore agent security group" \
--vpc-id vpc-12345678
# Add outbound rule to reach RDS
aws ec2 authorize-security-group-egress \
--group-id sg-agent123 \
--protocol tcp \
--port 5432 \
--source-group sg-rds456
# Add inbound rule to RDS security group
aws ec2 authorize-security-group-ingress \
--group-id sg-rds456 \
--protocol tcp \
--port 5432 \
--source-group sg-agent123Connecting to internal APIs (HTTP/HTTPS)
AgentCore agent security group (outbound rules):
Type: HTTPS, Port: 443, Destination: API security group or CIDR
Type: HTTP, Port: 80, Destination: API security group or CIDR (if needed)---
Step 3: Configure the agent for VPC
New project
agentcore create \
--name MyAgent \
--defaults \
--network-mode VPC \
--subnets subnet-abc123,subnet-def456 \
--security-groups sg-agent123Existing project
agentcore add agent \
--name MyAgent \
--network-mode VPC \
--subnets subnet-abc123,subnet-def456 \
--security-groups sg-agent123Or edit agentcore/agentcore.json directly — add the networkMode and networkConfig fields to the runtime's entry:
{
"runtimes": [
{
"name": "MyAgent",
"networkMode": "VPC",
"networkConfig": {
"subnets": ["subnet-abc123", "subnet-def456"],
"securityGroups": ["sg-agent123"]
}
}
]
}The $schema URL at the top of agentcore.json (https://schema.agentcore.aws.dev/v1/agentcore.json) gives IDE autocomplete and validation for every field — including the subnet/security-group ID patterns.
Deploy
agentcore deploy -y---
Internet access from VPC
[!WARNING]
Connecting AgentCore to a VPC does NOT provide internet access by default.
Public subnets do NOT provide internet access for AgentCore ENIs.
To reach the internet from VPC mode, you MUST use private subnets with a NAT gateway.
Architecture for internet + VPC access:
AgentCore agent (private subnet)
↓ outbound traffic
NAT Gateway (public subnet)
↓
Internet Gateway
↓
Internet# Create NAT gateway in a public subnet
aws ec2 create-nat-gateway \
--subnet-id subnet-public123 \
--allocation-id eipalloc-12345678
# Update private subnet route table to use NAT gateway
aws ec2 create-route \
--route-table-id rtb-private123 \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id nat-12345678---
Fully private VPC (no internet)
If your VPC has no internet access, you need VPC endpoints for AWS services. These endpoints are required without internet access and strongly recommended even with a NAT gateway to avoid NAT gateway data processing charges:
# ECR Docker endpoint (required for container image pulls)
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--service-name com.amazonaws.REGION.ecr.dkr \
--vpc-endpoint-type Interface \
--subnet-ids subnet-abc123 \
--security-group-ids sg-agent123
# ECR API endpoint (required for container image pulls)
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--service-name com.amazonaws.REGION.ecr.api \
--vpc-endpoint-type Interface \
--subnet-ids subnet-abc123 \
--security-group-ids sg-agent123
# S3 Gateway endpoint (required — ECR stores image layers in S3)
# This is a free Gateway endpoint. Without it, ECR image refreshes
# route through NAT and incur data processing charges.
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--service-name com.amazonaws.REGION.s3 \
--vpc-endpoint-type Gateway \
--route-table-ids rtb-private123
# CloudWatch Logs (required for agent logging)
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--service-name com.amazonaws.REGION.logs \
--vpc-endpoint-type Interface \
--subnet-ids subnet-abc123 \
--security-group-ids sg-agent123---
Cold-start connectivity checklist
A common pattern: UpdateAgentRuntime returns READY, the network configuration looks right, but invocations return 502 or hang. Requests never reach your container. This almost always means a new VM can start but can't complete the work needed to be ready for traffic.
Cold-start VMs need outbound HTTPS (port 443) to these AWS service endpoints. In public or NAT-routed VPCs, a correctly configured NAT gateway covers all of them. In fully private VPCs, every one of these needs an interface VPC endpoint or gateway endpoint:
com.amazonaws.<region>.ecr.api— pull image metadatacom.amazonaws.<region>.ecr.dkr— pull container layerscom.amazonaws.<region>.s3(Gateway endpoint) — ECR layers live in S3com.amazonaws.<region>.logs— emit CloudWatch logscom.amazonaws.<region>.monitoring— emit CloudWatch metricscom.amazonaws.<region>.sts— assume the execution role
Plus whichever endpoints your agent's tools and dependencies need (Bedrock, DynamoDB, Secrets Manager, etc.).
Security group outbound rule
The agent's security group needs an outbound rule to reach 443 on each VPC endpoint's prefix list, or 0.0.0.0/0 if the endpoints are reachable directly:
aws ec2 authorize-security-group-egress \
--group-id sg-agent123 \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0If you scope egress more tightly (to specific endpoint prefix lists or CIDR blocks), double-check that every endpoint above is covered.
NACLs — the gotcha
Network ACLs are stateless. A security group allowing outbound 443 implicitly allows the response traffic. A NACL does not.
If your subnet uses a restrictive NACL, you need both directions explicitly:
- Outbound: allow TCP 443 to the destination
- Inbound: allow ephemeral ports 1024–65535 (TCP) from the destination — these are the return-traffic ports
Forgetting the inbound ephemeral-port rule produces the exact symptom of "connection works sometimes, hangs other times" because TCP handshakes succeed (SYN goes out, SYN-ACK comes back on low port ranges) but the actual data response on an ephemeral port gets dropped.
Transit Gateway and custom egress
If your subnet routes outbound through a Transit Gateway to a central firewall, NAT, or network virtualization layer, the TGW attachment and downstream must have a working route to the internet (or to each VPC endpoint individually).
Symptoms of a missing TGW route:
- Invocations hang for the full client-side timeout (~300 seconds for default Lambda clients)
- No 502, no
ConnectionClosedError— the request just doesn't come back pingfrom a test EC2 in the same subnet/SG works, but actual invocations don't- Warm environments (already initialized, so already have all their egress done) succeed, new cold starts fail
The test-from-an-EC2 pattern is useful here: launch a t3.micro in the same subnet with the same security group, and try curl https://s3.<region>.amazonaws.com, curl https://ecr.<region>.amazonaws.com, etc. If any of those hang or fail, the agent will fail to cold-start too.
Expect higher cold-start time in VPC mode
VPC mode adds ENI attachment and setup time to cold start on top of container image pull and application startup. First invocations in a freshly-configured VPC are noticeably slower than in public mode.
Mitigation is the same as for all cold-start latency: reuse sessions, keep the image lean, defer heavy initialization. See agents-harden Initialization time section.
---
Troubleshooting
Connection timeouts to RDS or internal APIs:
1. Verify security group rules — outbound from agent SG, inbound on target SG 2. Check route tables — private subnet must route to NAT gateway (for internet) or have direct routes to targets 3. Verify DNS resolution is enabled in the VPC: aws ec2 describe-vpc-attribute --vpc-id vpc-12345678 --attribute enableDnsSupport
"Unsupported Availability Zone" error during deploy: Your subnet is in an AZ that AgentCore doesn't support. Check the AZ ID (not the AZ name) and use a subnet in a supported AZ.
Agent can't reach internet after VPC configuration: You're using a public subnet or missing a NAT gateway. AgentCore ENIs in public subnets don't get internet access. Use private subnets with a NAT gateway.
"AccessDenied" when using VPC endpoints: The execution role is missing permissions for the service behind the VPC endpoint. Check the endpoint's resource policy and the execution role's IAM policy.
Code Interpreter timeouts calling public endpoints: Code Interpreter also needs VPC configuration if your agent is in a VPC. Configure it with the same subnets and a NAT gateway for internet access.
DNS resolution failures: Enable DNS resolution and DNS hostnames in your VPC:
aws ec2 modify-vpc-attribute --vpc-id vpc-12345678 --enable-dns-support
aws ec2 modify-vpc-attribute --vpc-id vpc-12345678 --enable-dns-hostnamesOutput
- Subnet AZ validation results
- Security group rules for the specific target (RDS, internal API, etc.)
- CLI commands to configure VPC mode
- NAT gateway setup if internet access is needed
- VPC endpoint list for fully private deployments
Quality criteria
- Subnet AZ IDs are validated against supported AZs (not AZ names — names vary by account)
- Security group rules cover both directions (agent outbound + target inbound)
- NAT gateway is recommended for internet access (not public subnets — AgentCore ENIs don't get public IPs)
- VPC endpoint list is complete for fully private deployments
- The developer understands that
networkMode: VPCprimarily affects outbound traffic
Related skills
How it compares
Choose agents-build to create new AWS agent projects; pair with agents-connect when wiring external agents to AWS service connectors.
FAQ
What does agents-build do?
>
When should I use agents-build?
Invoke when >.
Is agents-build safe to install?
Review the Security Audits panel on this page before installing in production.