A Bedrock Agent without tools is just a chatbot. Action groups give your agent the ability to call APIs, query databases, and take real actions. Here's how to wire up Lambda functions with OpenAPI schemas using Terraform.
In the previous post, we deployed a Bedrock Agent that can reason and hold multi-turn conversations. But without tools, it can only generate text. Ask it to check inventory, create a ticket, or look up a customer, and it can only describe what it would do.
Action groups change that. An action group connects your agent to a Lambda function through an OpenAPI schema that describes the available operations. The agent reads the schema, decides which operation to call based on the user's request, extracts the required parameters from the conversation, and invokes the Lambda function. The agent becomes an actor, not just a talker. π―
ποΈ How Action Groups Work
User: "What's the exchange rate from USD to EUR?"
β
Agent reads OpenAPI schema β finds getExchangeRate operation
β
Agent extracts parameters: currency_from=USD, currency_to=EUR
β
Agent invokes Lambda function with parameters
β
Lambda calls external API, returns result
β
Agent formats response: "The current rate is 1 USD = 0.92 EUR"
The agent handles the reasoning. The OpenAPI schema describes what's possible. The Lambda function does the work. You define all three in Terraform.
π§ Two Schema Options
Bedrock supports two ways to define what your action group can do:
| Approach | Best For | Definition |
|---|---|---|
| OpenAPI schema | REST APIs with multiple endpoints | Full OpenAPI 3.0 spec with paths, methods, parameters |
| Function schema | Simple actions with clear parameters | Inline function definitions in Terraform |
OpenAPI schemas are more explicit and map directly to API operations. Function schemas are simpler for straightforward actions. This post covers both.
π§ Terraform: Action Group with OpenAPI Schema
The OpenAPI Schema
{
"openapi": "3.0.0",
"info": {
"title": "Exchange Rate API",
"version": "1.0.0"
},
"paths": {
"/exchange-rate": {
"get": {
"operationId": "getExchangeRate",
"description": "Get the current exchange rate between two currencies. Use this when the user asks about currency conversion or exchange rates.",
"parameters": [
{
"name": "currency_from",
"in": "query",
"required": true,
"schema": { "type": "string" },
"description": "The source currency code (e.g., USD, EUR, GBP)"
},
{
"name": "currency_to",
"in": "query",
"required": true,
"schema": { "type": "string" },
"description": "The target currency code (e.g., USD, EUR, GBP)"
}
],
"responses": {
"200": {
"description": "Exchange rate result",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"rate": { "type": "number" },
"from": { "type": "string" },
"to": { "type": "string" }
}
}
}
}
}
}
}
}
}
}
The description fields are critical. The agent uses these descriptions to decide when to call the operation and what parameters to extract. Vague descriptions lead to poor tool selection. Be specific about when the operation should be used.
Lambda Function
# lambda/exchange_rate/index.py
import json
import urllib.request
def handler(event, context):
# Bedrock passes the operation and parameters
api_path = event.get("apiPath")
parameters = {p["name"]: p["value"] for p in event.get("parameters", [])}
currency_from = parameters.get("currency_from", "USD")
currency_to = parameters.get("currency_to", "EUR")
# Call external API
url = f"https://api.frankfurter.dev/v1/latest?base={currency_from}&symbols={currency_to}"
req = urllib.request.Request(url)
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
rate = data["rates"].get(currency_to, "N/A")
# Return in Bedrock's expected format
return {
"messageVersion": "1.0",
"response": {
"actionGroup": event["actionGroup"],
"apiPath": api_path,
"httpMethod": event.get("httpMethod", "GET"),
"httpStatusCode": 200,
"responseBody": {
"application/json": {
"body": json.dumps({
"rate": rate,
"from": currency_from,
"to": currency_to
})
}
}
}
}
The response format matters. Bedrock expects a specific structure with messageVersion, response.actionGroup, and response.responseBody. Get this wrong and the agent receives empty results.
Terraform: Wire It All Together
# action_groups/lambda.tf
data "archive_file" "exchange_rate" {
type = "zip"
source_dir = "${path.module}/lambda/exchange_rate"
output_path = "${path.module}/lambda/exchange_rate.zip"
}
resource "aws_lambda_function" "exchange_rate" {
function_name = "${var.environment}-exchange-rate"
handler = "index.handler"
runtime = "python3.12"
timeout = 30
memory_size = 128
role = aws_iam_role.action_lambda.arn
filename = data.archive_file.exchange_rate.output_path
source_code_hash = data.archive_file.exchange_rate.output_base64sha256
}
# Allow Bedrock to invoke the Lambda
resource "aws_lambda_permission" "bedrock_invoke" {
statement_id = "AllowBedrockInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.exchange_rate.function_name
principal = "bedrock.amazonaws.com"
source_arn = aws_bedrockagent_agent.this.agent_arn
}
# action_groups/action_group.tf
resource "aws_bedrockagent_agent_action_group" "exchange_rate" {
action_group_name = "ExchangeRateAPI"
agent_id = aws_bedrockagent_agent.this.agent_id
agent_version = "DRAFT"
description = "Get current exchange rates between currencies"
action_group_executor {
lambda = aws_lambda_function.exchange_rate.arn
}
api_schema {
payload = file("${path.module}/schemas/exchange_rate.json")
}
skip_resource_in_use_check = true
}
After adding the action group, the agent must be re-prepared. If prepare_agent = true on the agent resource, Terraform handles this automatically for agent changes. For action group changes, add a null_resource to trigger re-preparation:
resource "null_resource" "prepare_after_action_group" {
triggers = {
action_group = aws_bedrockagent_agent_action_group.exchange_rate.id
}
provisioner "local-exec" {
command = "aws bedrock-agent prepare-agent --agent-id ${aws_bedrockagent_agent.this.agent_id} --region ${var.region}"
}
}
π§ Alternative: Function Schema (Simpler)
For straightforward actions, skip the OpenAPI file and define functions inline:
resource "aws_bedrockagent_agent_action_group" "weather" {
action_group_name = "WeatherLookup"
agent_id = aws_bedrockagent_agent.this.agent_id
agent_version = "DRAFT"
description = "Get current weather for a location"
action_group_executor {
lambda = aws_lambda_function.weather.arn
}
function_schema {
member_functions {
functions {
name = "getWeather"
description = "Get current weather conditions for a city"
parameters {
map_block_key = "city"
type = "string"
description = "City name (e.g., Seattle, London, Tokyo)"
required = true
}
parameters {
map_block_key = "units"
type = "string"
description = "Temperature units: celsius or fahrenheit"
required = false
}
}
}
}
}
No external schema file needed. Terraform defines everything inline. Good for one or two functions; use OpenAPI schemas when you have multiple endpoints.
π Multiple Action Groups
Agents can have multiple action groups, each with its own Lambda and schema. The agent decides which action group to use based on the user's request:
# Each action group = one capability domain
# ExchangeRateAPI β currency operations
# WeatherLookup β weather queries
# TicketManager β support ticket CRUD
Keep action groups focused on a single domain. An agent with 3 focused action groups performs better than one with 15 mixed operations because the model has clearer context for tool selection.
β οΈ Gotchas and Tips
Lambda response format. The most common error is returning a plain JSON response from Lambda instead of Bedrock's expected format with messageVersion and responseBody. The agent silently gets no result.
Description quality drives tool selection. The agent uses OpenAPI descriptions to decide which operation to invoke. Spend time writing clear, specific descriptions. Include when the operation should and should not be used.
Re-preparation after changes. Adding or modifying action groups requires re-preparing the agent. The null_resource pattern handles this in Terraform.
Tool name format. If using Claude models, tool names follow the format httpVerb__actionGroupName__apiName. Avoid double underscores in your action group or API names.
Return control option. Instead of Lambda execution, you can set custom_control = "RETURN_CONTROL" to have the agent return the operation and parameters to your application. Your app executes the action and sends the result back. Useful when the action requires client-side execution.
βοΈ What's Next
This is Post 2 of the AWS AI Agents with Terraform series.
- Post 1: Deploy First Bedrock Agent π€
- Post 2: Action Groups - Connect to APIs (you are here) π
- Post 3: Multi-Agent Orchestration
- Post 4: Agent + Knowledge Base Grounding
Your agent can now take action. It reads the schema, reasons about what to call, extracts parameters from conversation, and invokes your Lambda. The model thinks; the Lambda acts. π
Found this helpful? Follow for the full AI Agents with Terraform series! π¬
Top comments (0)