This is the third post in our Amazon Bedrock series, building on getting started and Knowledge Bases. It assumes the agent concepts from our LangChain and OpenAI series.
We've now built the "model decides to call a tool, your code executes it, the result feeds back in" pattern three times in this blog — directly against the OpenAI API, then with LangChain's tool-calling agents. A Bedrock Agent is AWS's managed version of the same underlying pattern, with Lambda functions as the actual tool implementations and IAM governing what each tool is permitted to do. Because a Bedrock Agent touches several distinct AWS services at once — Lambda, IAM, Knowledge Bases, and session state — it's worth covering the full architecture in real depth here.
Defining an action group: Bedrock's equivalent of a tool
{
"actionGroupName": "course-availability",
"description": "Check availability and details for NovuSpark training courses",
"actionGroupExecutor": {
"lambda": "arn:aws:lambda:eu-west-2:008971632408:function:check-course-availability"
},
"apiSchema": {
"payload": "{\"openapi\": \"3.0.0\", \"paths\": {\"/availability\": {\"get\": {\"parameters\": [{\"name\": \"course_name\", \"in\": \"query\", \"required\": true, \"schema\": {\"type\": \"string\"}}]}}}}"
}
}An action group is Bedrock's version of the @tool-decorated function from our LangChain series, or the JSON schema passed to tools in our OpenAI series — a description the agent uses to decide when this capability is relevant, paired here with an actual AWS Lambda function that executes when the agent decides to invoke it.
The Lambda function: where the real logic lives
def lambda_handler(event, context):
course_name = event["parameters"][0]["value"]
availability = {"Kubernetes Fundamentals": 4, "Terraform Fundamentals": 0}
seats = availability.get(course_name, "unknown")
return {
"response": {
"actionGroup": event["actionGroup"],
"apiPath": event["apiPath"],
"httpMethod": event["httpMethod"],
"httpStatusCode": 200,
"responseBody": {
"application/json": {
"body": f'{{"course_name": "{course_name}", "seats_available": {seats}}}'
}
},
}
}This is genuinely ordinary Lambda code — the same shape as any other Lambda function triggered by API Gateway or another AWS service — with the specific response format Bedrock Agents expect. The agent calls this function exactly the way a LangChain agent calls a Python tool function, or the OpenAI API calls back into your own code after a tool-call response — the mechanism is identical; only the specific plumbing connecting model decision to code execution differs.
Invoking the agent
client = boto3.client("bedrock-agent-runtime", region_name="eu-west-2")
response = client.invoke_agent(
agentId="AGENT123",
agentAliasId="ALIAS456",
sessionId="session-user-789",
inputText="Is there space in the Kubernetes course?",
)
for event in response["completion"]:
if "chunk" in event:
print(event["chunk"]["bytes"].decode("utf-8"), end="")Yes — there are 4 seats available in the Kubernetes Fundamentals course.
sessionId is doing real work here, not just an arbitrary identifier — Bedrock Agents maintain conversation state per session automatically, the managed equivalent of the RunnableWithMessageHistory session-keyed memory covered in our LangChain series, without you managing that state storage yourself.
The trace: seeing the agent's actual reasoning per step
Bedrock exposes a trace of the agent's reasoning, similar in spirit to the LangSmith tracing covered in our LangChain observability post — genuinely necessary for debugging why an agent chose a specific action group or produced an unexpected final answer:
response = client.invoke_agent(
agentId="AGENT123",
agentAliasId="ALIAS456",
sessionId="session-user-789",
inputText="Is there space in the Kubernetes course?",
enableTrace=True,
)
for event in response["completion"]:
if "trace" in event:
print(event["trace"]["trace"]){'orchestrationTrace': {'rationale': {'text': 'The user is asking about course
availability. I should call the course-availability action group.'}}}
enableTrace=True surfaces the agent's own stated rationale for each step — genuinely useful for confirming, or diagnosing, why it chose one action group over another, the same "inspect the actual trace instead of guessing" instinct covered for LangChain in an earlier series, applied here to a managed AWS agent rather than a self-hosted one.
Combining action groups with a Knowledge Base
{
"agentName": "novuspark-support-agent",
"actionGroups": ["course-availability", "enrollment"],
"knowledgeBases": [
{
"knowledgeBaseId": "KB123ABC",
"description": "Company policies and course catalog documentation"
}
]
}A single Bedrock Agent can draw on both action groups (Lambda-backed tools for taking actions or fetching live data) and Knowledge Bases (retrieval over static documentation, covered in the previous post) simultaneously — deciding, per user message, whether the right response comes from calling a tool, retrieving a policy document, or both. This mirrors the multi-agent, multi-tool coordination patterns covered in our LangGraph series, provided here as a managed AWS capability rather than something you assemble and operate yourself.
Multi-agent collaboration: Bedrock's supervisor pattern
Beyond a single agent with multiple action groups, Bedrock also supports multi-agent collaboration — a supervisor agent that delegates specific sub-tasks to specialized collaborator agents, the same specialization principle covered for LangGraph's supervisor pattern in an earlier series:
{
"agentCollaboration": "SUPERVISOR",
"collaborators": [
{
"collaboratorName": "ResearchAgent",
"agentDescriptor": {"aliasArn": "arn:aws:bedrock:eu-west-2:008971632408:agent-alias/RESEARCH123/ALIASA"},
"collaborationInstruction": "Delegate research and fact-gathering questions to this agent."
},
{
"collaboratorName": "SupportAgent",
"agentDescriptor": {"aliasArn": "arn:aws:bedrock:eu-west-2:008971632408:agent-alias/SUPPORT456/ALIASB"},
"collaborationInstruction": "Delegate customer support and enrollment questions to this agent."
}
]
}A supervisor agent configured this way decides, per incoming message, which specialized collaborator agent is best suited to handle it — the managed AWS equivalent of the LangGraph supervisor-and-specialist pattern, with each collaborator agent independently configurable with its own action groups, Knowledge Bases, and even its own underlying foundation model, exactly the same per-specialist model-choice freedom flagged as a real advantage of multi-agent systems in our LangGraph series.
Governance: the same IAM discipline, applied to what an agent can actually do
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:eu-west-2:008971632408:function:check-course-availability"
}
]
}The agent's own execution role should be scoped to invoke only the specific Lambda functions backing its own defined action groups — not a blanket lambda:InvokeFunction on every function in the account. This is the same least-privilege discipline covered for our own GitHub Actions deploy pipeline, and flagged for LangChain agents taking consequential actions: an agent's actual permissions should match exactly what it needs, independent of trusting its judgment about when to use them.
Safeguarding consequential actions inside the Lambda function itself
The same principle covered for LangChain tools in an earlier series — enforce real limits inside a tool's own code, not just in its description — applies identically to a Bedrock action group's backing Lambda function:
def lambda_handler(event, context):
refund_amount = float(event["parameters"][0]["value"])
if refund_amount > 100:
return build_response(event, 200, {
"status": "requires_manual_approval",
"message": "Amount exceeds automatic refund limit."
})
process_refund(...)
return build_response(event, 200, {"status": "processed"})The Lambda function itself — not the action group's OpenAPI schema description, and not the agent's system prompt — is what genuinely enforces the $100 limit here. A schema or prompt is a description the model reasons from; the actual if refund_amount > 100 check inside real, executing code is what an attacker or a misguided agent decision can't simply talk its way around.
What to actually remember from this post
- A Bedrock action group is the managed equivalent of a tool in LangChain or the OpenAI API — a schema the agent reasons about, backed by a real Lambda function that executes.
- The Lambda function is genuinely ordinary Lambda code, following a specific response shape Bedrock expects — the same tool-execution pattern covered throughout this blog, just running as a managed AWS function.
sessionIdprovides managed conversation memory, the equivalent of LangChain's session-keyed message history, without self-managing that storage.enableTrace=Truesurfaces the agent's own stated reasoning per step — the managed-AWS equivalent of LangChain tracing, worth using before guessing at why an agent behaved a certain way.- Multi-agent collaboration supports a supervisor delegating to specialized collaborator agents, each with its own action groups, Knowledge Bases, and even model choice — the same specialization principle from LangGraph, as a managed capability.
- Scope an agent's IAM execution role to exactly the Lambda functions its own action groups need, and enforce genuinely consequential limits inside the Lambda code itself — least privilege applied to what the agent can actually do, not just what it's designed to attempt.
Next in the series: Amazon Bedrock Guardrails: Responsible AI in Production, where we cover the safety and content-filtering layer that sits around everything covered so far in this series.
