Wiring an Action Group to Lambda With Bedrock Agents
10 min read · updated August 11, 2026
If you are searching for this, you are one of two people: someone maintaining an agent that already exists, or someone about to build a new one. The second person should stop and read the next section first. The first person can skip to the event contract, which is the part that is genuinely under-documented and unchanged.
Read this before you build
Amazon Bedrock Agents has been renamed Amazon Bedrock Agents Classic and is no longer open to new customers. AWS’s own action-group documentation now opens with that notice and points new work at Amazon Bedrock AgentCore, while stating that existing customers can continue to use the service as normal.
What that means practically: the API still works, your agents still run, and nothing here is deprecated out from under you today. But it is in maintenance mode, which is a poor foundation for something new, and a greenfield project should be looking at AgentCore or at running its own tool loop over the Converse API. A hand-written loop is perhaps forty lines and you own every decision in it, which for most teams turns out to be an advantage rather than a cost.
What an action group is
An action group is a named set of operations the agent may perform, plus a description of how to perform them. It has two halves.
The definition tells the model what exists, and comes in two forms. An API schema is an OpenAPI document, stored in S3 or inlined as a payload, from which the agent learns paths, methods and parameters. Function details are a simpler list of named functions with typed parameters and no HTTP vocabulary at all. Which one you pick changes the event shape your Lambda receives, so it is not a cosmetic choice.
The executor tells the agent what to call — actionGroupExecutor with a Lambda ARN. There is also a returnControl mode where the agent hands the call back to your application instead of invoking anything, which is the right choice when the action needs credentials or context that should not live in a Lambda.
One hard limit that shapes design: AWS documents that an action group can contain up to 11 API operations, but you write only one Lambda function for it. That function receives one operation per invocation and must dispatch internally. Eleven operations in one handler is the maximum the service will let you build, not a recommendation.
The Lambda event contract
This is the part worth having written down, because getting the response shape wrong produces an agent that fails without saying why. With an API schema, the event Bedrock sends looks like this:
{
"messageVersion": "1.0",
"agent": {"name": "...", "id": "...", "alias": "...", "version": "..."},
"inputText": "what is the status of order 91824",
"sessionId": "...",
"actionGroup": "OrderActions",
"apiPath": "/orders/{orderId}",
"httpMethod": "GET",
"parameters": [{"name": "orderId", "type": "string", "value": "91824"}],
"requestBody": {"content": {"application/json": {"properties": []}}},
"sessionAttributes": {},
"promptSessionAttributes": {}
}With function details instead of a schema, apiPath, httpMethod and requestBody are absent and a single function field carries the function name. Everything else is the same. messageVersion is 1.0 and AWS states that is the only version supported.
The response must mirror the definition style you chose. For an API schema:
def lambda_handler(event, context):
order_id = next(
p["value"] for p in event.get("parameters", []) if p["name"] == "orderId"
)
status = lookup_order(order_id)
return {
"messageVersion": "1.0",
"response": {
"actionGroup": event["actionGroup"],
"apiPath": event["apiPath"],
"httpMethod": event["httpMethod"],
"httpStatusCode": 200,
"responseBody": {
"application/json": {"body": json.dumps({"status": status})}
},
},
"sessionAttributes": event.get("sessionAttributes", {}),
"promptSessionAttributes": event.get("promptSessionAttributes", {}),
}Note that body is a JSON-formatted string, not a JSON object. Returning a dict there is the single most common cause of an action group that appears to run and produces nothing the model can use.
The function-details response is shaped differently and carries a field the API-schema form does not have. Under response.functionResponse you may set responseState to one of two documented values, and the difference is significant:
FAILURE— the agent throws aDependencyFailedExceptionfor the session. Use it when a downstream dependency broke. The conversation ends.REPROMPT— the agent passes your response string back to the model to reprompt it. Use it when the input was invalid: the model gets a chance to ask the user for a valid order number instead of the whole turn dying.
Choosing REPROMPT for validation errors and FAILURE for genuine outages is most of the difference between an agent that recovers and one that gives up. Also remember that the response is bounded by Lambda’s own synchronous invocation payload quota — an action group returning a large query result will hit that, and the fix is pagination in your handler, not a bigger Lambda.
Session state travels through here too. sessionAttributes persist for the session; promptSessionAttributes persist for one turn. Echoing them back unchanged, as above, is the minimum; writing to them is how you keep state without a database for short-lived context like a selected account id.
The permission that is not IAM
The agent’s service role needs the usual Bedrock permissions, but the invocation itself is authorised on the Lambda side. AWS documents that you must attach a resource-based policy to the function granting the agent permission to invoke it. An identity policy on the agent role is not enough, and this is the failure that looks like the model refusing to call your tool.
aws lambda add-permission \ --function-name order-actions \ --statement-id bedrock-agent-invoke \ --action lambda:InvokeFunction \ --principal bedrock.amazonaws.com \ --source-arn arn:aws:bedrock:us-east-1:111122223333:agent/ABCDEFGHIJ \ --source-account 111122223333
Scope it with --source-arn to the specific agent. A bare principal grant lets any Bedrock agent in any account invoke your function, which is a confused-deputy problem with a business-logic Lambda on the other end of it.
Reading one turn end to end
InvokeAgent with enableTrace set returns trace events alongside the response chunks, and that is the only way to see what the agent decided and why.
- Call
InvokeAgentwithenableTrace: true, anagentId, anagentAliasId, asessionIdandinputText. The response is an event stream, so you iterate it. - Read the orchestration trace. Its
modelInvocationInputshows the prompt the agent actually built — including your action group descriptions — and itsrationaleshows the reasoning that led to a tool choice. Nearly every “why did it not call my function” question is answered by reading the descriptions as they appear here. - Read the
invocationInput, which names the action group and the parameters being sent. Compare it against what your Lambda received in CloudWatch; if they match and the turn still failed, the problem is your response shape. - Read the
observation, which carries what came back and how it was folded into the next model call. - If you attached a guardrail, check the guardrail trace before concluding the model misbehaved — a blocked intermediate step looks a lot like a model that lost the thread.
Prepare the agent after every change. Edits to an action group or its schema do not reach the working draft until PrepareAgent runs, and aliases point at versions rather than at the draft. An agent that stubbornly behaves the way it did before your edit has almost always not been prepared, or you are invoking an alias pinned to the previous version.