AI Inventory Tracker with AWS Serverless
Build a serverless AI inventory system using Lambda, DynamoDB, and Bedrock.
Introduction
30 Second Summary
Every business tracks inventory. Whether it's a warehouse full of electronics or an office supply closet, someone needs to know what's in stock and what's running low.
In this project, you will build a serverless AI inventory tracker on AWS that lets you ask questions about your stock in plain English. You'll connect multiple cloud services together into a system that interprets natural language queries and returns structured results.
What You'll Build
A cloud-native inventory system that stores products in DynamoDB and uses Amazon Bedrock's Nova Micro model to interpret natural language queries about your inventory.
By the end of this project, you'll have:
- A DynamoDB table storing inventory items with names, categories, quantities, prices, and restock dates.
- A Lambda function that handles CRUD operations and AI-powered natural language queries via Bedrock.
- A REST API endpoint through API Gateway that accepts both structured inventory operations and plain English questions.
- Secret Mission: A low-stock alerting feature that uses AI to generate restock recommendation reports with priority rankings.
Are there any prerequisites?
You'll need an AWS account (Free Tier eligible) and basic Python knowledge. If you don't have an AWS account yet, you'll create one in Step 1.
Set Up Your AWS Environment
Before you can build a serverless inventory tracker, you need the right tools on your machine. Python runs the code, the AWS CLI talks to AWS from your terminal, and boto3 is the Python library that connects the two together.
In this step, you will install and configure each of these tools so your local machine is ready to create cloud resources.
In this step, get ready to:
- Install Python 3.10+ and boto3.
- Install and verify AWS CLI v2.
- Configure AWS CLI with your credentials and default region.
Install Python and boto3
Python is the language your Lambda function will be written in. The boto3 library is the official AWS SDK for Python, and it requires Python 3.10 or higher.
- Check your Python version by running this command:
python --version
✔️ I see version 3.10 or higher
Your Python version is compatible. You are ready to install boto3.
ⓧ I see a version lower than 3.10
You need to upgrade to Python 3.10 or higher.
- Visit the official Python downloads page:
- https://www.python.org/downloads/
- Download the latest Python 3 installer for your operating system.
- Run the installer and follow the prompts.
- On Windows, make sure to check the box that says Add Python to PATH before clicking Install Now.
- After installation, close and reopen your terminal, then run python --version again to confirm.
ⓧ Command not found
Python is not installed on your machine yet.
- Visit the official Python downloads page:
- https://www.python.org/downloads/
- Download the latest Python 3 installer for your operating system.
- Run the installer and follow the prompts.
- On Windows, make sure to check the box that says Add Python to PATH before clicking Install Now.
- On macOS, the installer will guide you through the process automatically.
- After installation, close and reopen your terminal, then run python --version to verify.
macOS users may need python3
On macOS, the command may be python3 --version instead of python --version. If python does not work, try python3 for all commands in this project.
- Install boto3 by running this command:
python -m pip install boto3
What does this command do?
This installs boto3, the official AWS SDK for Python. Your Lambda function and seed script both use boto3 to talk to AWS services like DynamoDB and Amazon Bedrock.
pip command not found?
If you get a pip error, try python3 -m pip install boto3 instead. On some systems, pip is only available through the python -m pip pattern.
Help me fix my pip installation.
Install AWS CLI v2
The AWS CLI lets you create and manage AWS resources directly from your terminal. You will use it throughout this project to create your DynamoDB table, deploy your Lambda function, and set up API Gateway.
macOS
- Download and install the AWS CLI v2 package by running these commands:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
What do these commands do?
The first command downloads the official AWS CLI installer package from Amazon. The second command runs the macOS installer, which places the aws binary in /usr/local/bin.
Your terminal will ask for your system password because of sudo. Type it and press Enter (the characters will not appear as you type).
Windows
- Download the AWS CLI v2 MSI installer from this URL:
- https://awscli.amazonaws.com/AWSCLIV2.msi
- Run the downloaded AWSCLIV2.msi file.
- Follow the installer wizard prompts, accepting the defaults.
- Close and reopen your terminal after installation completes.
- Verify the installation by running:
aws --version
✔️ I see aws-cli/2.x.x
AWS CLI v2 is installed and ready.
ⓧ I see aws-cli/1.x.x
You have AWS CLI version 1 installed. You need version 2 for this project.
- Uninstall version 1 first, then follow the installation steps above for your platform.
- After reinstalling, close and reopen your terminal, then run aws --version again.
ⓧ Command not found
The installation did not complete, or your terminal cannot find the aws command.
- Make sure you closed and reopened your terminal after installation.
- On macOS, check that /usr/local/bin is in your PATH.
- On Windows, try opening a new Command Prompt or PowerShell window.
- If it still fails, re-run the installer from the instructions above.
Configure AWS CLI with your credentials
The AWS CLI needs your account credentials to create resources on your behalf. You will provide an Access Key ID, a Secret Access Key, and a default region.
Where do I get my Access Key?
You create Access Keys in the IAM section of the AWS Management Console.
- Sign in to the AWS Management Console.
- Search for IAM in the top search bar and open it.
- Click Users in the left sidebar, then select your user.
- Click the Security credentials tab.
- Under Access keys, click Create access key.
- Select Command Line Interface (CLI) as the use case.
- Copy your Access Key ID and Secret Access Key somewhere safe. You will not be able to see the Secret Access Key again.
- Configure the AWS CLI by running:
aws configure
The command will prompt you for four values. Enter them one at a time:
- For AWS Access Key ID: paste your Access Key ID.
- For AWS Secret Access Key: paste your Secret Access Key.
- For Default region name: type us-east-1.
- For Default output format: type json.
Why us-east-1?
This project uses us-east-1 because Amazon Bedrock and Nova Micro are available in this region. Using a consistent region for all your resources avoids cross-region latency and permission issues.
- Confirm your configuration worked by running:
aws sts get-caller-identity
You should see a JSON response with your account ID, user ARN, and user ID.
Seeing an error about credentials?
Make sure you pasted the Access Key ID and Secret Access Key without any extra spaces. Run aws configure again to re-enter them if needed.
Help me fix my AWS CLI credential error.
- Open your code editor by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code (or Cursor if you use Cursor) and press Enter to open it.
Your local environment is fully configured. Next up, you will create the DynamoDB table that stores your inventory data.
Create the DynamoDB Table
Your environment is ready. Now it's time to create the database that stores your inventory.
In this step, you'll provision a DynamoDB table, populate it with sample inventory data using a Python script, and verify everything landed correctly.
In this step, get ready to:
- Create a DynamoDB table with a partition key.
- Seed the table with sample inventory items using boto3.
- Verify the data by scanning the table via the AWS CLI.
Create the InventoryItems table
Every item in your inventory needs a unique identifier. DynamoDB uses a partition key to distribute items across storage partitions and retrieve them efficiently.
- Create the DynamoDB table by running this command in your terminal:
aws dynamodb create-table \
--table-name InventoryItems \
--attribute-definitions AttributeName=item_id,AttributeType=S \
--key-schema AttributeName=item_id,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5
What does this command do?
This creates a table called InventoryItems in your configured region (us-east-1).
- The --attribute-definitions declares item_id as a String (S) attribute.
- The --key-schema sets item_id as the HASH (partition) key, meaning each item is uniquely identified by this field.
- The --provisioned-throughput sets 5 read and 5 write capacity units. This stays well within the DynamoDB Always Free tier of 25 RCU and 25 WCU.
- Wait about 10 seconds for the table to become active, then verify its status by running:
aws dynamodb describe-table --table-name InventoryItems --query "Table.TableStatus"
You should see "ACTIVE" in the output.
Seeing CREATING instead of ACTIVE?
DynamoDB tables take a few seconds to provision. Wait 10-15 seconds and run the describe-table command again.
If you see an error about the table not existing, double-check your region. Run aws configure get region to confirm it returns us-east-1.
help me troubleshoot my DynamoDB table creation
Seed the table with sample inventory data
An empty table is no good for testing AI queries later. You need realistic inventory items across different categories so the AI has something meaningful to analyze.
- Switch back to VS Code.
- Create a new file called seed_data.py in your project folder.
- Add the following code to seed_data.py:
import boto3
import uuid
from datetime import datetime, timedelta
import random
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
table = dynamodb.Table("InventoryItems")
items = [
{
"item_id": str(uuid.uuid4()),
"name": "Wireless Mouse",
"category": "Electronics",
"quantity": 45,
"price": "29.99",
"last_restocked": "2026-05-15",
},
{
"item_id": str(uuid.uuid4()),
"name": "USB-C Hub",
"category": "Electronics",
"quantity": 8,
"price": "49.99",
"last_restocked": "2026-04-20",
},
{
"item_id": str(uuid.uuid4()),
"name": "Sticky Notes Pack",
"category": "Office Supplies",
"quantity": 120,
"price": "4.99",
"last_restocked": "2026-06-01",
},
{
"item_id": str(uuid.uuid4()),
"name": "Whiteboard Markers",
"category": "Office Supplies",
"quantity": 3,
"price": "12.99",
"last_restocked": "2026-03-10",
},
{
"item_id": str(uuid.uuid4()),
"name": "Coffee Maker",
"category": "Kitchen",
"quantity": 2,
"price": "89.99",
"last_restocked": "2026-01-22",
},
{
"item_id": str(uuid.uuid4()),
"name": "Standing Desk Mat",
"category": "Office Supplies",
"quantity": 15,
"price": "34.99",
"last_restocked": "2026-05-28",
},
]
for item in items:
table.put_item(Item=item)
print(f"Added: {item['name']}")
print(f"\nSuccessfully added {len(items)} items to InventoryItems table.")
What does this script do?
This script uses boto3 (the AWS Python SDK) to connect to your DynamoDB table and insert 6 sample items.
- Each item gets a random UUID as its item_id partition key.
- Items span three categories: Electronics, Office Supplies, and Kitchen.
- Quantities vary widely (from 2 to 120) so later AI queries about low-stock items return interesting results.
- Save seed_data.py.
- Run the script by executing this command in your terminal:
python seed_data.py
You should see each item name printed as it gets added, followed by a success message confirming all 6 items were inserted.
Seeing a ResourceNotFoundException?
This means the table hasn't finished creating yet. Wait a few more seconds and try running the script again.
If you see a NoCredentialsError, your AWS credentials aren't configured for Python. Make sure aws configure completed successfully in Step 1.
help me fix my seed_data.py script errors
✔️ Awesome, I've got everything!
Great. Double check you've saved seed_data.py and that the terminal output shows all 6 items added successfully.
ⓧ I'd like to double check the full code
Here is the complete seed_data.py file for comparison:
import boto3
import uuid
from datetime import datetime, timedelta
import random
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
table = dynamodb.Table("InventoryItems")
items = [
{
"item_id": str(uuid.uuid4()),
"name": "Wireless Mouse",
"category": "Electronics",
"quantity": 45,
"price": "29.99",
"last_restocked": "2026-05-15",
},
{
"item_id": str(uuid.uuid4()),
"name": "USB-C Hub",
"category": "Electronics",
"quantity": 8,
"price": "49.99",
"last_restocked": "2026-04-20",
},
{
"item_id": str(uuid.uuid4()),
"name": "Sticky Notes Pack",
"category": "Office Supplies",
"quantity": 120,
"price": "4.99",
"last_restocked": "2026-06-01",
},
{
"item_id": str(uuid.uuid4()),
"name": "Whiteboard Markers",
"category": "Office Supplies",
"quantity": 3,
"price": "12.99",
"last_restocked": "2026-03-10",
},
{
"item_id": str(uuid.uuid4()),
"name": "Coffee Maker",
"category": "Kitchen",
"quantity": 2,
"price": "89.99",
"last_restocked": "2026-01-22",
},
{
"item_id": str(uuid.uuid4()),
"name": "Standing Desk Mat",
"category": "Office Supplies",
"quantity": 15,
"price": "34.99",
"last_restocked": "2026-05-28",
},
]
for item in items:
table.put_item(Item=item)
print(f"Added: {item['name']}")
print(f"\nSuccessfully added {len(items)} items to InventoryItems table.")
Verify the data with a table scan
Before moving on, confirm that all 6 items are actually sitting in DynamoDB. A table scan retrieves every item in the table.
- Scan the table by running this command:
aws dynamodb scan --table-name InventoryItems --query "Count"
You should see 6 in the output, confirming all items were written successfully.
Why scan instead of query?
A scan reads every item in the table. For a small demo table with 6 items, this is perfectly fine and the simplest way to verify your data.
In production systems with millions of rows, you would use a targeted query with indexes instead. The AI layer you'll build in the next steps abstracts this complexity for end users.
Seeing 0 items or an error?
If the count is 0, the seed script may not have completed successfully. Re-run python seed_data.py and check for error messages.
If you see an access denied error, confirm your IAM user has DynamoDB permissions. The credentials you configured in Step 1 need at least dynamodb:Scan and dynamodb:PutItem permissions.
help me verify my DynamoDB table has data
Your database is loaded with inventory data. Next up, you'll build the Lambda function that reads from this table and adds AI-powered natural language querying.
Build the Lambda Function
Your DynamoDB table is loaded with inventory data. Now you need the compute layer that reads and writes to that table.
In this step, you will write a Lambda function that handles standard inventory operations (list, add, update) and connects to Amazon Bedrock for AI-powered natural language queries. You will also create the IAM role that gives Lambda permission to access DynamoDB and Bedrock, then deploy everything to AWS.
In this step, get ready to:
- Create a Lambda function with CRUD inventory operations.
- Add an AI query handler using the Bedrock Converse API.
- Deploy the function to AWS with proper IAM permissions.
Create the Lambda function code for CRUD operations
The Lambda function acts as a router. It receives HTTP requests from API Gateway and dispatches them to the correct handler based on the path and method.
- In your code editor, create a new file called lambda_function.py in the same project folder where seed_data.py lives.
- Add the following code to set up imports, AWS clients, and the routing logic:
import json
import boto3
from boto3.dynamodb.conditions import Attr
from decimal import Decimal
# Connect to DynamoDB and Bedrock
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
table = dynamodb.Table("InventoryItems")
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
# The AI model we'll use for natural language queries
MODEL_ID = "amazon.nova-micro-v1:0"
# DynamoDB stores numbers as Decimal, but JSON can't serialize Decimal
class DecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)
def lambda_handler(event, context):
# Extract HTTP method and path from the API Gateway event
http_method = event.get("httpMethod", "GET")
path = event.get("path", "/inventory")
body = json.loads(event.get("body", "{}")) if event.get("body") else {}
# Route to the correct handler based on path and method
if path == "/query" and http_method == "POST":
return handle_ai_query(body)
elif path == "/inventory":
if http_method == "GET":
return list_items()
elif http_method == "POST":
return add_item(body)
elif path.startswith("/inventory/") and http_method == "PUT":
item_id = path.split("/")[-1]
return update_quantity(item_id, body)
return response(404, {"error": "Not found"})
What does this code do?
This sets up the foundation of your Lambda function.
- boto3.resource("dynamodb") creates a high-level DynamoDB client for reading and writing items.
- boto3.client("bedrock-runtime") creates the Bedrock client you will use later for AI queries.
- DecimalEncoder solves a common issue where DynamoDB returns numbers as Python Decimal objects that JSON cannot serialize directly.
- lambda_handler is the entry point AWS calls. It routes each request to the right function based on the URL path and HTTP method.
- Below the lambda_handler function, add the CRUD handler functions and the response helper:
def list_items():
# Scan returns all items in the table
result = table.scan()
return response(200, {"items": result["Items"]})
def add_item(body):
# Validate that all required fields are present
required = ["item_id", "name", "category", "quantity", "price"]
if not all(k in body for k in required):
return response(400, {"error": f"Missing required fields: {required}"})
table.put_item(Item=body)
return response(201, {"message": "Item added", "item": body})
def update_quantity(item_id, body):
# Update only the quantity field for an existing item
new_quantity = body.get("quantity")
if new_quantity is None:
return response(400, {"error": "quantity is required"})
table.update_item(
Key={"item_id": item_id},
UpdateExpression="SET quantity = :q",
ExpressionAttributeValues={":q": new_quantity},
)
return response(200, {"message": "Quantity updated", "item_id": item_id})
def response(status_code, body):
# Standard API Gateway response format with CORS headers
return {
"statusCode": status_code,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
"body": json.dumps(body, cls=DecimalEncoder),
}
What do these functions do?
- list_items() scans the entire DynamoDB table and returns all inventory items.
- add_item() validates that required fields exist, then writes a new item to the table.
- update_quantity() changes just the quantity of an existing item using an update expression.
- response() formats every return value in the structure API Gateway expects, including CORS headers so browsers can call the API.
- Save lambda_function.py.
You will not see any output yet. The next substep adds the AI query handler, then you will deploy everything together.
Add the Bedrock integration using the Converse API
The AI query layer is what makes this project special. Instead of writing complex DynamoDB filter expressions, users can ask questions in plain English and let the AI figure out how to answer them.
Why the Converse API?
AWS recommends the Converse API over InvokeModel because it provides a unified interface that works the same way across all Bedrock models. If you ever swap Nova Micro for a different model, your code stays the same.
- In lambda_function.py, add the following function between update_quantity and response:
def handle_ai_query(body):
# Get the natural language question from the request
user_query = body.get("query", "")
if not user_query:
return response(400, {"error": "query field is required"})
# Fetch all inventory items to give the AI full context
inventory_data = table.scan()
items = inventory_data["Items"]
inventory_summary = json.dumps(items, cls=DecimalEncoder, indent=2)
# Tell the AI what it is and what data it has access to
system_prompt = (
"You are an inventory management assistant. "
"You have access to the following inventory data. "
"Answer the user's question based on this data. "
"Return your answer as JSON with a 'results' array containing matching items "
"and a 'summary' field with a brief natural language answer.\n\n"
f"INVENTORY DATA:\n{inventory_summary}"
)
# Format the user's question for the Converse API
messages = [{"role": "user", "content": [{"text": user_query}]}]
# Call Bedrock's Converse API with Nova Micro
bedrock_response = bedrock.converse(
modelId=MODEL_ID,
system=[{"text": system_prompt}],
messages=messages,
inferenceConfig={"maxTokens": 1024, "temperature": 0.3},
)
# Extract the AI's text response
ai_text = bedrock_response["output"]["message"]["content"][0]["text"]
return response(200, {"query": user_query, "ai_response": ai_text})
What does handle_ai_query do?
This function takes a natural language question, fetches all inventory data, and sends both to Amazon Nova Micro via the Converse API.
- The system_prompt tells the AI its role and provides the full inventory dataset as context.
- temperature: 0.3 keeps responses focused and consistent rather than creative.
- The AI returns a structured JSON answer with matching items and a natural language summary.
- Save lambda_function.py.
Your Lambda function code is now complete.
✔️ Awesome, I've got everything!
Great. Double check you have saved lambda_function.py before moving on.
ⓧ I'd like to double check the full code
import json
import boto3
from boto3.dynamodb.conditions import Attr
from decimal import Decimal
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
table = dynamodb.Table("InventoryItems")
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL_ID = "amazon.nova-micro-v1:0"
class DecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)
def lambda_handler(event, context):
http_method = event.get("httpMethod", "GET")
path = event.get("path", "/inventory")
body = json.loads(event.get("body", "{}")) if event.get("body") else {}
if path == "/query" and http_method == "POST":
return handle_ai_query(body)
elif path == "/inventory":
if http_method == "GET":
return list_items()
elif http_method == "POST":
return add_item(body)
elif path.startswith("/inventory/") and http_method == "PUT":
item_id = path.split("/")[-1]
return update_quantity(item_id, body)
return response(404, {"error": "Not found"})
def list_items():
result = table.scan()
return response(200, {"items": result["Items"]})
def add_item(body):
required = ["item_id", "name", "category", "quantity", "price"]
if not all(k in body for k in required):
return response(400, {"error": f"Missing required fields: {required}"})
table.put_item(Item=body)
return response(201, {"message": "Item added", "item": body})
def update_quantity(item_id, body):
new_quantity = body.get("quantity")
if new_quantity is None:
return response(400, {"error": "quantity is required"})
table.update_item(
Key={"item_id": item_id},
UpdateExpression="SET quantity = :q",
ExpressionAttributeValues={":q": new_quantity},
)
return response(200, {"message": "Quantity updated", "item_id": item_id})
def handle_ai_query(body):
user_query = body.get("query", "")
if not user_query:
return response(400, {"error": "query field is required"})
inventory_data = table.scan()
items = inventory_data["Items"]
inventory_summary = json.dumps(items, cls=DecimalEncoder, indent=2)
system_prompt = (
"You are an inventory management assistant. "
"You have access to the following inventory data. "
"Answer the user's question based on this data. "
"Return your answer as JSON with a 'results' array containing matching items "
"and a 'summary' field with a brief natural language answer.\n\n"
f"INVENTORY DATA:\n{inventory_summary}"
)
messages = [{"role": "user", "content": [{"text": user_query}]}]
bedrock_response = bedrock.converse(
modelId=MODEL_ID,
system=[{"text": system_prompt}],
messages=messages,
inferenceConfig={"maxTokens": 1024, "temperature": 0.3},
)
ai_text = bedrock_response["output"]["message"]["content"][0]["text"]
return response(200, {"query": user_query, "ai_response": ai_text})
def response(status_code, body):
return {
"statusCode": status_code,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
"body": json.dumps(body, cls=DecimalEncoder),
}
Do I need to enable Bedrock model access?
Access to Amazon Bedrock foundation models is enabled by default for accounts with the correct AWS Marketplace permissions in commercial regions. For most accounts, Nova Micro works immediately.
If you hit an access error when testing later, open the Amazon Bedrock console, choose Model access under Bedrock configurations in the left navigation pane, choose Enable specific models, select the Amazon Nova models, and submit.
Package and deploy the Lambda function with the IAM role
Before Lambda can run your function, it needs an IAM role that grants it permission to access DynamoDB, Bedrock, and CloudWatch Logs. You will create two policy files, build the role, then zip and deploy your function.
- Create a file called trust_policy.json in the same folder as lambda_function.py and paste this content:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
What is a trust policy?
A trust policy tells AWS which service is allowed to "assume" (use) this role. Here, you are saying only the Lambda service can use it. Without this, Lambda would not be able to run your function code under the role's permissions.
- Save trust_policy.json.
- Create another file called permissions_policy.json and paste this content:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:Scan",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/InventoryItems"
},
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel"
],
"Resource": "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-micro-v1:0"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:*:*"
}
]
}
What does this permissions policy grant?
This follows the principle of least privilege. Each statement grants only the minimum actions needed.
- Statement 1 allows DynamoDB operations only on the InventoryItems table.
- Statement 2 allows Bedrock to invoke only the Nova Micro model.
- Statement 3 allows Lambda to write logs to CloudWatch for debugging.
- Save permissions_policy.json.
- Confirm all three files exist by running:
ls
You should see lambda_function.py, trust_policy.json, permissions_policy.json, and seed_data.py in the output.
- Create the IAM role by running this command:
aws iam create-role --role-name inventory-tracker-role --assume-role-policy-document file://trust_policy.json
What should I see?
You should see a JSON response containing the role's details, including an Arn field that looks like arn:aws:iam::123456789012:role/inventory-tracker-role. Copy this ARN. You will need it in a moment.
- Attach the permissions policy to the role by running:
aws iam put-role-policy --role-name inventory-tracker-role --policy-name inventory-tracker-permissions --policy-document file://permissions_policy.json
This command produces no output on success. No news is good news.
- Zip your Lambda function code by running the command for your operating system:
macOS
zip function.zip lambda_function.py
Windows
Compress-Archive -Path lambda_function.py -DestinationPath function.zip
- Deploy the Lambda function by running this command, replacing the role ARN with the one you copied earlier:
aws lambda create-function --function-name inventory-tracker --runtime python3.10 --role [[ROLE_ARN="arn:aws:iam::123456789012:role/inventory-tracker-role"]] --handler lambda_function.lambda_handler --zip-file fileb://function.zip
What do these flags mean?
- --function-name sets the name of your Lambda function in AWS.
- --runtime python3.10 tells Lambda which Python version to use.
- --handler lambda_function.lambda_handler points to the file (lambda_function) and function (lambda_handler) that Lambda calls when triggered.
- --zip-file fileb:// tells the CLI to upload the binary zip file directly.
You should see a JSON response containing your function's ARN, runtime, and handler details. This confirms your Lambda function is deployed.
Seeing an error about the role?
- If you see a message about the role not being found or not being assumable, wait 10 seconds and try the command again. IAM roles take a few seconds to propagate across AWS.
- If you see a message about the function already existing, you may have run the command twice. Run aws lambda delete-function --function-name inventory-tracker and try again.
- Double check that your --role value is the full ARN starting with arn:aws:iam:: from the create-role output.
help me deploy my Lambda function
Your Lambda function is live on AWS with all the permissions it needs. Next up, you will connect it to API Gateway so it can receive HTTP requests from the outside world.
Connect API Gateway
Your Lambda function is deployed and ready to handle requests. The missing piece is a public URL that routes HTTP traffic to it.
In this step you will create an API Gateway REST API that exposes two paths: /inventory for structured CRUD operations and /query for natural language AI queries. Then you will deploy it to a prod stage so you can call it from anywhere.
In this step, get ready to:
- Create a REST API with resources and methods connected to your Lambda function.
- Deploy the API to a public stage.
- Test the endpoints with curl or PowerShell.
Create the REST API and resources
API Gateway needs a top-level API object, then individual resources (URL paths) under it. Each resource gets HTTP methods attached, and each method is wired to your Lambda function via a proxy integration.
- Create the REST API by running this command:
aws apigateway create-rest-api --name 'inventory-api' --region us-east-1
What does this command do?
This creates a new REST API object in API Gateway named inventory-api. The output includes an id field. You will use this ID in every following command.
- Copy the id value from the output. You will need it for every subsequent command in this step.
- Get the root resource ID by running this command (replace YOUR_API_ID with the ID you just copied):
aws apigateway get-resources --rest-api-id [[API_ID="your API ID from the previous command"]]
What does this command do?
Every REST API starts with a root resource (/). You need its id to create child resources beneath it.
- Copy the id of the root resource (the one with path set to /).
- Create the /inventory resource by running:
aws apigateway create-resource --rest-api-id [[API_ID="your API ID"]] --parent-id [[ROOT_ID="your root resource ID"]] --path-part inventory
- Copy the id from the output. This is your inventory resource ID.
- Create the /query resource by running:
aws apigateway create-resource --rest-api-id [[API_ID="your API ID"]] --parent-id [[ROOT_ID="your root resource ID"]] --path-part query
- Copy the id from the output. This is your query resource ID.
Keep track of your IDs
You now have three IDs: the API ID, the inventory resource ID, and the query resource ID. Keep them in a text file or note so you can paste them into the next commands quickly.
Getting an error about the API or resource not existing?
Double check that you are pasting the correct ID for each placeholder. The --rest-api-id is the API ID (from create-rest-api) and --parent-id is the root resource ID (from get-resources).
help me troubleshoot API Gateway resource creation errors
Add methods and Lambda integration
Each resource needs HTTP methods (GET, POST) attached, and each method needs an integration that tells API Gateway where to forward the request. You will use Lambda proxy integration, which passes the full HTTP request to your Lambda function.
- Add a GET method to the /inventory resource by running:
aws apigateway put-method --rest-api-id [[API_ID="your API ID"]] --resource-id [[INVENTORY_RESOURCE_ID="your inventory resource ID"]] --http-method GET --authorization-type "NONE"
- Connect the GET method to your Lambda function by running:
aws apigateway put-integration --rest-api-id [[API_ID="your API ID"]] --resource-id [[INVENTORY_RESOURCE_ID="your inventory resource ID"]] --http-method GET --type AWS_PROXY --integration-http-method POST --uri "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:[[AWS_ACCOUNT_ID="your AWS account ID"]]:function:inventory-tracker/invocations"
- Add a POST method to the /inventory resource by running:
aws apigateway put-method --rest-api-id [[API_ID="your API ID"]] --resource-id [[INVENTORY_RESOURCE_ID="your inventory resource ID"]] --http-method POST --authorization-type "NONE"
- Connect the POST method to your Lambda function by running:
aws apigateway put-integration --rest-api-id [[API_ID="your API ID"]] --resource-id [[INVENTORY_RESOURCE_ID="your inventory resource ID"]] --http-method POST --type AWS_PROXY --integration-http-method POST --uri "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:[[AWS_ACCOUNT_ID="your AWS account ID"]]:function:inventory-tracker/invocations"
- Add a POST method to the /query resource by running:
aws apigateway put-method --rest-api-id [[API_ID="your API ID"]] --resource-id [[QUERY_RESOURCE_ID="your query resource ID"]] --http-method POST --authorization-type "NONE"
- Connect the POST method to your Lambda function by running:
aws apigateway put-integration --rest-api-id [[API_ID="your API ID"]] --resource-id [[QUERY_RESOURCE_ID="your query resource ID"]] --http-method POST --type AWS_PROXY --integration-http-method POST --uri "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:[[AWS_ACCOUNT_ID="your AWS account ID"]]:function:inventory-tracker/invocations"
Why is integration-http-method always POST?
Lambda proxy integrations always use POST as the integration HTTP method because API Gateway sends the entire request payload to Lambda via a POST call to the Lambda invoke API. The external-facing HTTP method (GET, POST, PUT) is passed inside the event object so your function can route on it.
Where do I find my AWS account ID?
Run aws sts get-caller-identity in your terminal. The Account field is your 12-digit account ID.
Seeing a "not found" error on put-integration?
Make sure you ran put-method first for the same resource and HTTP method. The method must exist before you can attach an integration to it.
help me fix my put-integration error in API Gateway
Deploy the API and grant Lambda permission
Your API has resources and methods configured, but it is not reachable from the internet yet. You need to deploy it to a stage and then grant API Gateway permission to invoke your Lambda function.
- Deploy the API to a prod stage by running:
aws apigateway create-deployment --rest-api-id [[API_ID="your API ID"]] --stage-name prod
What is a stage?
A stage is a named snapshot of your API configuration. Deploying to the prod stage creates a public URL at https://{api-id}.execute-api.us-east-1.amazonaws.com/prod. You can have multiple stages (dev, staging, prod) pointing to different configurations.
- Grant API Gateway permission to invoke your Lambda function by running:
aws lambda add-permission --function-name inventory-tracker --statement-id apigateway-invoke --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn "arn:aws:execute-api:us-east-1:[[AWS_ACCOUNT_ID="your AWS account ID"]]:[[API_ID="your API ID"]]/*/*/*"
What does this command do?
This adds a resource-based policy to your Lambda function allowing the API Gateway service to call it. The --source-arn with /*/*/* grants permission for any stage, any method, and any resource on this API.
- Note your invoke URL. It follows this pattern:
echo "https://[[API_ID="your API ID"]].execute-api.us-east-1.amazonaws.com/prod"
- Test the /inventory endpoint by running:
curl https://[[API_ID="your API ID"]].execute-api.us-east-1.amazonaws.com/prod/inventory
Using PowerShell instead of curl?
On Windows without curl, use Invoke-WebRequest instead:
Invoke-WebRequest -Uri "https://[[API_ID="your API ID"]].execute-api.us-east-1.amazonaws.com/prod/inventory"
You should see a JSON response containing your inventory items. The output includes all six items you seeded in the DynamoDB table earlier.
- Test the /query endpoint with a natural language question by running:
curl -X POST https://[[API_ID="your API ID"]].execute-api.us-east-1.amazonaws.com/prod/query -H "Content-Type: application/json" -d '{"query": "What items have quantity less than 10?"}'
You should see a JSON response with query and ai_response fields. The AI response lists the low-stock items from your inventory.
Getting a 500 Internal Server Error?
This usually means API Gateway does not have permission to invoke your Lambda function. Confirm the add-permission command ran successfully. Also verify that your account ID and API ID in the --source-arn are correct.
If you see an error about Bedrock access, make sure you enabled the Amazon Nova Micro model in the Bedrock console (Model Access section).
help me debug my API Gateway 500 error with Lambda
Your API is live. Anyone with the URL can now query your inventory system. Next up, you will put the AI query layer through its paces with different natural language questions.
Test the AI Query Layer
Your API is live and your Lambda function is connected to Amazon Bedrock. Now it is time to put the AI query layer through its paces.
The /query endpoint accepts plain English questions about your inventory and uses Nova Micro to interpret them. In this step, you will send several natural language queries, verify the AI returns accurate results, and explore how it handles tricky edge cases.
In this step, get ready to:
- Send natural language queries through the API and receive AI-generated answers.
- Verify the AI correctly interprets questions and returns matching inventory data.
- Test edge cases like ambiguous queries and empty results.
Send your first natural language query
Your /query endpoint accepts a POST request with a JSON body containing a query field. The AI reads your entire inventory, interprets the question, and returns a structured JSON answer.
- Send a natural language query to your API by running this command:
macOS
curl -X POST [[INVOKE_URL="your API Gateway invoke URL"]]/query \
-H "Content-Type: application/json" \
-d '{"query": "What items have quantity less than 10?"}'
Windows
$body = '{"query": "What items have quantity less than 10?"}'
Invoke-WebRequest -Uri "[[INVOKE_URL="your API Gateway invoke URL"]]/query" -Method POST -Body $body -ContentType "application/json"
What does this command do?
This sends a POST request to your /query endpoint with the question "What items have quantity less than 10?" in the request body.
Your Lambda function receives the question, scans the InventoryItems table, sends all the inventory data plus your question to Nova Micro, and returns the AI's structured answer.
- Check the response in your terminal.
- You should see a JSON response containing a query field (echoing your question) and an ai_response field with the AI's answer.
What should the AI response look like?
The ai_response field contains a JSON string with a results array and a summary field. For this query, the results should include items like USB-C Hub (quantity 8), Whiteboard Markers (quantity 3), and Coffee Maker (quantity 2).
Getting a timeout or error?
If you see a 500 error, check that you enabled model access for Amazon Nova Micro in the Bedrock console. New accounts need to request access first (usually instant for Nova models).
If you see a connection error, double check your invoke URL is correct. It should end with /prod/query and not have a trailing slash before query.
help me debug my /query endpoint error
Run more queries to explore your inventory
One query proves the connection works. Multiple queries prove the AI actually understands your data.
- Ask for the most expensive item by running this command:
macOS
curl -X POST [[INVOKE_URL="your API Gateway invoke URL"]]/query \
-H "Content-Type: application/json" \
-d '{"query": "Show me the most expensive item"}'
Windows
$body = '{"query": "Show me the most expensive item"}'
Invoke-WebRequest -Uri "[[INVOKE_URL="your API Gateway invoke URL"]]/query" -Method POST -Body $body -ContentType "application/json"
- Verify the response mentions Coffee Maker at $89.99.
- Now query by category by running this command:
macOS
curl -X POST [[INVOKE_URL="your API Gateway invoke URL"]]/query \
-H "Content-Type: application/json" \
-d '{"query": "List all office supplies"}'
Windows
$body = '{"query": "List all office supplies"}'
Invoke-WebRequest -Uri "[[INVOKE_URL="your API Gateway invoke URL"]]/query" -Method POST -Body $body -ContentType "application/json"
- Verify the response includes Sticky Notes Pack, Whiteboard Markers, and Standing Desk Mat.
Why does the AI get these right?
Your Lambda function sends the entire inventory dataset to Nova Micro as context in the system prompt. The AI reads all six items and filters them based on your question. This works well for small datasets like yours.
For production systems with thousands of items, you would pre-filter with a DynamoDB query first and only send relevant items to the AI.
Test edge cases and ambiguous queries
Real users ask imprecise questions. Testing edge cases helps you understand how the AI handles ambiguity and missing data.
- Send an ambiguous query by running this command:
macOS
curl -X POST [[INVOKE_URL="your API Gateway invoke URL"]]/query \
-H "Content-Type: application/json" \
-d '{"query": "What should I reorder soon?"}'
Windows
$body = '{"query": "What should I reorder soon?"}'
Invoke-WebRequest -Uri "[[INVOKE_URL="your API Gateway invoke URL"]]/query" -Method POST -Body $body -ContentType "application/json"
What happens with ambiguous queries?
Nova Micro interprets "reorder soon" by reasoning about which items have low quantities. It should surface items like Coffee Maker (quantity 2) and Whiteboard Markers (quantity 3). The AI applies common-sense logic even when your question does not specify an exact threshold.
- Now test a query that should return no matches by running this command:
macOS
curl -X POST [[INVOKE_URL="your API Gateway invoke URL"]]/query \
-H "Content-Type: application/json" \
-d '{"query": "Show me all items in the Automotive category"}'
Windows
$body = '{"query": "Show me all items in the Automotive category"}'
Invoke-WebRequest -Uri "[[INVOKE_URL="your API Gateway invoke URL"]]/query" -Method POST -Body $body -ContentType "application/json"
- Verify the AI responds with an empty results set and a summary explaining that no Automotive items exist in the inventory.
How does the AI handle empty results?
Your inventory has no Automotive category items. Instead of crashing or returning an error, the AI recognizes there are no matches and tells you so in natural language. This graceful handling is built into the system prompt you wrote in lambda_function.py.
AI giving unexpected answers?
AI responses have some natural variation. The exact wording and formatting may differ between runs. What matters is that the correct items are identified and the summary is accurate.
If the AI consistently returns wrong items or ignores your question, check the system_prompt in lambda_function.py. Make sure it includes the instruction to return JSON with a results array and a summary field.
help me understand why my AI query returns unexpected results
Secret mission
Add Low-Stock AI Alerting
Your inventory system is smart, but it's reactive. What if it could proactively warn you when items are running low and recommend what to restock first? In this secret mission, you'll build an AI-powered alerting endpoint that scans your inventory for low-stock items and generates prioritized restock recommendations.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. DynamoDB and Lambda are both on the AWS Always Free tier, so leaving them idle costs nothing.
Cost warning
Amazon Bedrock charges per query. Nova Micro costs $0.000035 per 1,000 input tokens and $0.00014 per 1,000 output tokens. A typical session during this project costs less than $0.01. API Gateway's free tier (1 million calls/month) expires after 12 months. If you leave the API deployed beyond that window, you may incur charges at $3.50 per million requests.
Resources you used:
- DynamoDB table InventoryItems (Always Free tier: 25 GB storage, 25 RCU/WCU).
- Lambda function inventory-tracker (Always Free tier: 1M requests/month).
- API Gateway REST API (Free tier: 1M API calls/month for 12 months).
- IAM role inventory-tracker-role with inline permissions policy.
- Local project files (lambda_function.py, seed_data.py, trust_policy.json, permissions_policy.json).
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the project further.
- DynamoDB and Lambda stay within the Always Free tier indefinitely. You will not be charged for idle resources.
- API Gateway's free tier covers 1 million calls per month for the first 12 months. After that, charges apply only when the API is invoked.
- Bedrock charges only when you send queries. No idle cost.
Pause - I'll come back to this later
Shut down running processes to free up resources. With serverless architecture, there is nothing actively running to pause.
- Serverless resources (Lambda, DynamoDB, API Gateway) only cost when used. There is no server to stop.
- If you want to prevent accidental API calls, you can delete the API Gateway deployment stage. This disables the endpoint without removing the full configuration.
- Your local files remain on your machine until you delete them manually.
Delete - I don't want to use this again
Remove all project resources and start fresh. Run these commands in your terminal in the order shown.
- Delete the API Gateway REST API by running:
aws apigateway delete-rest-api --rest-api-id [[API_ID="your API Gateway REST API ID"]]
- Delete the Lambda function by running:
aws lambda delete-function --function-name inventory-tracker
- Delete the inline policy from the IAM role, then delete the role itself by running:
aws iam delete-role-policy --role-name inventory-tracker-role --policy-name inventory-tracker-permissions
aws iam delete-role --role-name inventory-tracker-role
- Delete the DynamoDB table by running:
aws dynamodb delete-table --table-name InventoryItems
- Delete your local project files by running:
rm lambda_function.py seed_data.py trust_policy.json permissions_policy.json
How do I find my API Gateway REST API ID?
Run the following command to list your REST APIs and find the ID:
aws apigateway get-rest-apis
Look for the API with the name you gave it during creation. The id field is what you need for the delete command.
Nice Work!
Nice Work!
Nice work! You just built a cloud-native AI inventory system from scratch using four AWS managed services.
You've learned how to:
- Build a serverless architecture connecting Lambda, DynamoDB, Bedrock, and API Gateway with least-privilege IAM permissions.
- Use Amazon Bedrock's Nova Micro model and the Converse API to add AI-powered natural language querying to a NoSQL database.
- Create a REST API that handles both structured data operations and AI-powered queries through a single Lambda function.
- Add an intelligent low-stock alerting system that generates AI-powered restock recommendations with priority rankings.
Ready to quiz yourself?