Building an AI Agent with AWS Bedrock for U.S. National Parks

Learn to build an AI agent with AWS Bedrock and the National Parks API in this hands-on guide to agentic software development.

Mountainous landscape of Glacier National Park with dense forests under a clear blue sky.
A sweeping view of Glacier National Park Photo Credit: NPS/David Restivo, sourced from National Park Service.

Introduction

After attending AWS re:Invent 2024, I was most intrigued by the emergence of agentic software—a transformative technology reshaping how we build and interact with software systems. I previously shared my excitement in How AWS re:Invent 2024 Sparked My Journey into Agentic Software. At its core, agentic software acts autonomously on users' behalf, making decisions and interfacing with users, APIs, and other agentic systems to accomplish specific tasks. This paradigm shift opens up new possibilities for software development, and these agentic systems may become the primary consumers of many of the APIs and data structures we're building today.

In this post, I'll guide you through building a practical example: an agent that interfaces with the U.S. National Park Service (NPS) API. This agent enables users to inquire about National Parks using natural language, demonstrating agentic systems. While our NPS agent has a narrow scope, it is a realistic example of agent development and showcases how AWS technologies can jumpstart your journey into building agentic software.

If you wish to follow along while reading this post, you can view the examples-ai-bedrock-agent-national-parks repository on GitHub. This project leverages AWS CDK to deploy the example, the Amazon Bedrock Agent framework for building the agent, AWS Lambda, and the U.S. National Park Service API. Since this project uses CDK for Infrastructure as Code (IaC), you can deploy it to your AWS account to explore further.

Amazon Bedrock Agents Primer

Amazon Bedrock Agents is a framework designed to streamline the development of agentic software. Using consistent APIs, developers can easily switch between various foundation models (FMs) for reasoning and natural language processing. Bedrock supports models from many popular providers, including Anthorpic, Meta, poolside, and Amazon (see the complete list here). This flexibility is helpful when evaluating which model offers the optimal balance of price and performance for specific tasks. Beyond model selection, Bedrock also provides prompt management, offers observability through detailed trace logs, and supports multi-agent collaboration, pre-built templates, and configurable guardrails to promote responsible AI use.

Amazon Bedrock Agents integrate seamlessly with other AWS services, such as IAM and CloudWatch, ensuring a consistent development experience. Amazon's Bedrock User Guide provides a comprehensive overview of how Bedrock Agents function. AWS's guide is a must-read before starting with Bedrock.

There are several ways to configure and deploy a Bedrock Agent: you can create an agent directly in the AWS Console, dynamically using the experimental InvokeInlineAgent API, or via infrastructure as code (IaC). The manual approach is a great way to familiarize with Bedrock Agents, while InvokeInlineAgent enables faster evaluation of multiple configurations. I chose the IaC route for this example project, using Amazon CDK to deploy my agent. AWS Labs offers a generative-ai-constructs repository with several experimental L2 and L3 constructs that simplify the deployment of pattern-based AI solutions. I found these constructs intuitive and easy to use, so this is the path I took.

Why the U.S. National Parks Service API?

I wanted to build a practical AI agent using Amazon Bedrock that could interact with a real-world API. After some Googling, I discovered the U.S. National Park Service has a free, public API. The U.S. National Park Service (NPS) API proved to be an ideal choice, as it's well-documented and provides rich data about national parks, including their locations, weather conditions, activities, alerts, facilities, and even webcams and photos. This API offered an excellent opportunity to learn how to develop a Bedrock Agent that could provide meaningful responses to user queries about national parks.

The NPS API requires registration for an API key and imposes a default limit of 1,000 requests per hour. While this quota is sufficient for development and testing, a production deployment would benefit from implementing a caching solution such as Momento, AWS ElastiCache, or Valkey to manage higher traffic volumes efficiently.

The NPS API provides relatively static information and minimal dynamic information, such as weather. Building a knowledge base with this information would be more sensible and cost-effective for a production application and could help work around API throttling rates. However, I sought to gain experience creating agents that interact with APIs. I wanted to force this agent to call the NPS API to answer the user's questions about parks.

Agent in Action

Before diving into how the Parks Agent was developed, let’s first look at some examples of how it works. The National Park Agent allows users to ask natural language questions about parks. I named this agent "Bob" and instructed it to be helpful by answering users' questions using the NPS API. Users can ask questions like: "What parks are in Ohio?", "What parks in Michigan can I go to for kayaking?", "Which park in Michigan is the least expensive?", or "Do any parks in Utah have snow?". A more advanced version of this agent could even reserve camping sites or activity spaces for users if the NPS API supported POST or PUT requests.

Example Agent Conversation

An AI chat about Michigan parks for kayaking suggests Isle Royale National Park and Pictured Rocks National Lakeshore.
An example conversation with between a user and the NPS Agent

When a user asks our agent a simple query, like "What parks are in Michigan?" many things happen in the background.

  1. Pre-processing:
    1. The FM contextualizes the user's input to understand this is a query about park locations within a specific state (Michigan).
    2. The agent follows its prompt instructions and determines that it can proceed based on instructions. It is hinted that Michigan may need to be converted to a two-digit state code (MI) when calling the NPS API.
  2. Orchestration:
    1. The FM interprets the input and reasons for which actions should be taken next. The chain of reasoning is available in the trace.
    2. The agent predicts it needs to use the NPS API action group, including an OpenAPI spec that documents how the API works.
    3. The agent determines from the spec what parameters it needs to execute a search request using the API (MI for stateCode).
    4. Since all required parameters are in the user's question (the state = Michigan), the agent doesn't need to prompt the user for additional information.
    5. The agent invokes the AWS Lambda function configured in the action group, passing the necessary parameters to query the NPS API.
    6. The agent generates an observation based on the API response containing the Michigan National Parks list.
    7. The FM then interprets this observation to determine if it has enough information to provide a complete response or needs another orchestration iteration.
  3. Post-processing
    1. The agent formats the final response about Michigan's national parks in a user-friendly way before returning it to the user. The primary prompt instructions can be requested in a specific format, such as JSON or Markdown.


For more advanced queries, like "Which parks in the US are ADA accessible?", several processing iterations may occur before a final response is returned to the user.

Occasionally, agents can behave in unexpected ways. Bedrock provides traces, which provide observability into the agent's reasoning. The image below includes an excerpt of a trace for the example query:

Agent Session Trace Example

You can view a complete trace for a user's query in examples/example-session-trace.json.

NPS Agent Instruction Prompt

Our NPS agent is guided by a primary prompt that defines its behavior. Below is an example of how this prompt appears in the AWS Bedrock Agent Console. Due to its strong agentic support, this example uses the Anthropic Claude 3 Sonnet model as the agent's FM.

The instruction prompt is critical to the agent's overall effectiveness. For production applications, prompts should be carefully crafted and rigorously evaluated to ensure optimal performance. Since this was a prompt for an example project, I only spent enough time on this prompt to make it perform reasonably well.

A screenshot of a configuration screen for the Bedrock Agent that configures the instruction prompt for the agent.
NPS Agent Instruction Prompt

NPS Action Group

For the NPS agent, we define a single action group. This action group is configured with an OpenAPI 3.0.1 spec describing the API for accessing NPS data. The spec in this example defines a search route and a detailed route that can be used by the helpful park agent. Next, we configure this action group to route requests to a lambda that can handle search and detail requests. This lambda then routes requests to the NPS API using our API key and returns a response to the agent.

When providing API specs to action groups, it's essential to give as much information and context as possible so that the agent can make better-reasoned decisions.

The screenshot below highlights how the action group was configured with an in-line OpenAPI schema that defines the operations the lambda supports. The complete schema is in parks-api-schema.json.

Amazon Bedrock Agent configuration screen showing the OpenAPI schema input section for defining available API operations
NPS Action Group Definition with OpenAPI Spec

The second action group screenshot shows how it was configured to route requests to a get-national-park-info lambda using requests that adhere to the OpenAPI spec. The lambda is a monolithic lambdalith that handles both search and detail requests.

Amazon Bedrock Agent configuration showing NPS action group settings with Lambda function and OpenAPI schema integration
NPS Action Group configured to invoke a GET Lambda using the spec

Note: As of 1/2025, Lambda's responses to Bedrock are limited to a maximum payload response of 25KB. It's important to ensure that the responses returned by the action group invoked Lambdas fall under this limit and follow the response format documented in the Amazon Bedrock Lambda response event guide. Despite the 25KB limit for a single response, the agent may invoke an API multiple times to obtain the required information to complete a request. My first version of this agent quickly hit this limit, and I had to modify the Lambda to only return essential details.

CDK Constructs

The NPS Agent can be deployed using the AWS Cloud Development Kit (CDK) with AWS Generative AI CDK Constructs to simplify deployment. The application consists of two stacks: the first deploys the Amazon Bedrock Agent, and the second deploys the Lambda function that handles NPS API interactions.

Project Structure


├── bin/ # CDK entry point
│ ├── national-parks-bedrock-agent-example
├── lib/ # CDK IaC
| ├── schema # Contains an Open API 3.0.1 spec we share with the Action Group
│ ├── stacks/ # CDK stacks
│  ├── bedrock-agent-stack.ts # Deploys a Bedrock Agent, Action Group, and sets up IAM
│  ├── national-parks-api-stack.ts # Deploys Lambda that interacts with NPS API
├── src/
│ ├── clients/ # API clients
│ │ └── nps/ # National Parks Service API client directory
│ └── functions/ # Lambda functions
│   └── parks/ # Parks info Lambda

CDK Entry Point

The entry point, bin/national-parks-bedrock-agent-example.ts, provides the instruction prompt to the BedrockAgentStack:


// excerpt 
const agentInstruction = `
You are Bob, an expert virtual park ranger assistant who helps people discover and learn about United States National Parks. Your mission is to make exploring parks accessible and exciting for everyone.

Core Functions:
1. Search for National Parks by state using two-letter state codes (e.g., MI = Michigan)
2. Provide detailed information about specific parks
3. Answer questions about the National Park system

API Integration Rules:
- Use the stateCode parameter for state-specific park searches
- Use parkCode from search results when fetching detailed park information
- Always verify API responses before sharing information

Information Presentation:
- For state searches: Display park name, location, and a brief compelling description
- For detailed park queries: Include full name, official website URL, description, and state location
- Format responses in clear sections with proper spacing
- Include direct links to official park resources when available

Voice and Personality:
- Always start conversations with: "Bob here! I'd love to help you explore our National Parks. What would you like to know?"
- Communicate with authentic ranger-like enthusiasm and expertise
- Use clear, accessible language while maintaining technical accuracy
- Share interesting facts that spark curiosity
- Express genuine passion for conservation and park exploration

Error Handling:
- If state code is invalid, explain proper format and provide an example
- If park code isn't found, suggest similar parks or verify input
- Always maintain helpful demeanor when clarifying user requests

Remember to:
- Use "National Park" or "National Parks" consistently in formal names
- Keep descriptions factual but engaging
- Encourage responsible park visitation
- Respect official park designations and terminology
`;

const bedrockAgentStack = new BedrockAgentStack(
   app,
   `${APP_NAME}-BedrockAgentStack`,
   {
      env,
      agentName: 'national-parks-info-agent',
      foundationModel:
         bedrock.BedrockFoundationModel.ANTHROPIC_CLAUDE_SONNET_V1_0,
      agentInstruction: agentInstruction,
      getNationalParksInfoApiFunctionArn:
         nationalParksApiStack.getParksInfoApiFunction.functionArn,
      tags: COMMON_TAGS,
   },
);

Bedrock Agent Stack

The heart of our agent lives in lib/stacks/bedrock-agent-stack.ts. Using the Generative AI Bedrock CDK, this stack handles all the heavy lifting: setting up permissions, crafting the agent's instruction prompt, and defining its single action group. The stack also defines an OpenAPI specification and configures the lambda function it should call when interacting with the defined API.

     // excerpt 
     const getNationalParksInfoApiFunction =
         lambda.Function.fromFunctionAttributes(
            this,
            'GetNationalParksInfoApiFunctionArn',
            {
               functionArn: props.getNationalParksInfoApiFunctionArn,
               sameEnvironment: true,
            },
         );

      getNationalParksInfoApiFunction.grantInvoke(agentExecutionRole);

      this.bedrockAgent = new bedrock.Agent(this, 'Agent', {
         name: props.agentName,
         description: 'The Bedrock Agent for US National Parks information',
         foundationModel: props.foundationModel,
         instruction: props.agentInstruction,

         idleSessionTTL: cdk.Duration.minutes(30),
         enableUserInput: true,
         shouldPrepareAgent: true,
         existingRole: agentExecutionRole,
      });

      // create the action group for interacting with the National Parks info lambda
      this.parkInfoActionGroup = new AgentActionGroup(
         this,
         'ParkInfoActionGroup',
         {
            actionGroupName: 'national-parks-info-action-group',
            description:
               'Action group for retrieving National Parks information',
            actionGroupExecutor: {
               lambda: getNationalParksInfoApiFunction,
            },
            actionGroupState: 'ENABLED',
            // refer to https://docs.aws.amazon.com/bedrock/latest/userguide/agents-api-schema.html
            // to se requirements on Action Group schemas paths, method, description, operationId,
            // and responses are minimally required
            apiSchema: ApiSchema.fromInline(JSON.stringify(parksApiSchema)),
         },
      );

      this.bedrockAgent.addActionGroups([this.parkInfoActionGroup]);

National Parks API Stack

The lib/stacks/national-parks-api-stack.ts deploys a monolithic lambda that responds to search and detail requests. We provide our secret API key to this lambda. For now, this is being passed as an environmental secret. I would consider using a secret manager, such as AWS SSM, for a production app to store sensitive information. The stack is configured to enable Powertools for AWS Lambda (TypeScript) for improved logging and observability.

      
      //excerpt
      if (!props.npsApiKey) {
         throw new Error('The NPS API Key (npsApiKey) is required.');
      }

      /**
       * Defines the Lambda function that facilitates requests to the US National Parks API.
       */
      this.getParksInfoApiFunction = new nodeLambda.NodejsFunction(
         this,
         'GetNationalParksInfoFunction',
         {
            architecture: lambda.Architecture.ARM_64,
            runtime: lambda.Runtime.NODEJS_22_X,
            memorySize: 2048,
            timeout: Duration.seconds(60),
            // Since this isn't a production app, we don't need to store logs long term
            logRetention: RetentionDays.THREE_DAYS,
            functionName: `${cdk.Stack.of(this).stackName}-get-national-park-info`,
            description:
               'Lambda function that facilitates requesting park information from the US National Parks API',
            entry: join(
               __dirname,
               '../../src/functions/parks/parks-info-handler.ts',
            ),
            handler: 'handler',
            environment: {
               // NOTE: this API key isn't super sensitive, but we could consider storing this in secrets manager
               NPS_API_KEY: props.npsApiKey,
               POWERTOOLS_LOGGER_LOG_EVENT: 'true',
               POWERTOOLS_LOGGER_LOG_LEVEL: 'TRACE',
               POWERTOOLS_LOGGER_SAMPLE_RATE: '1',
               POWERTOOLS_SERVICE_NAME: 'national-parks-service',
            },
            bundling: {
               sourceMap: true,
               format: nodeLambda.OutputFormat.ESM,
               environment: { NODE_ENV: 'production' },
               externalModules: ['aws-sdk'],
            },
            currentVersionOptions: {
               removalPolicy: cdk.RemovalPolicy.DESTROY,
               retryAttempts: 1,
            },
         },
      );
      

Parks Get Info Lambda

The request handler is a straightforward TypeScript Lambda that manages park search and detail requests. While the NPS API offers extensive data, our Lambda returns a subset of the information available to stay within Bedrock's 25KB response limit. The lambda uses a custom client to interact with the NPS API (authenticated via an API key). I generated this client by having Anthropic Claude analyze the NPS API specification and create the TypeScript implementation.

Deploying and Testing

The examples-ai-bedrock-agent-national-parks repository includes a ReadMe with instructions for deploying this application in your account. The easiest way to use this agent is to use the test client directly within the Bedrock AWS Console. Alternatively, you can access the published agent using a REST client tool, such as Postman or curl, with proper authorization.

Future Enhancements

If I were to develop this example application further, there would be several areas for improvement.

First, I would incorporate Bedrock Guardrails to enhance security. While the NPS API only exposes GET operations, which reduces abuse concerns, adding guardrails would provide an extra layer of protection. I noticed that the leading FMs already have many safeguards to prevent misuse upfront, but additional safety should be added for production applications.

Second, I would implement caching for dynamic NPS content to avoid hitting API throttle limits. I would build a knowledge base for more static park content to reduce dependency on the NPS API for each request.

Lastly, I would add multiple action groups and maybe even consider using a router to leverage different agents for particular tasks.

The InvokeInlineAgent API could help iterate on this agent and test various configurations to determine the optimal setup for production use. I would like to explore using this API to evaluate the performance of different configurations.

Conclusion

This example demonstrates building a practical, functional AI agent using Amazon's Bedrock framework. Builders can easily experiment with different FMs while using consistent APIs. The framework's seamless integration with AWS services and robust observability features make it ideal for building production-ready AI applications.

In future blog posts, I plan to explore other AI topics, including AI observability, practices, knowledge base construction, and sophisticated design patterns for building complex autonomous agents that go beyond the capabilities of this NPS example.

Resources