AWS Builder Center

Build an AI Agent in 10 Minutes

Build a TypeScript AI calendar agent with Strands Agents and Claude Haiku 4.5 on Amazon Bedrock in about 10 minutes. It books, moves, checks and cancels meetings in plain English, and it never double-books, because the overlap check lives in the tool's code, not just the prompt. You'll set up least-privilege IAM, run a one-command Bedrock pre-flight check, clone and test the app, watch it refuse a real clash, and fix the six errors you're most likely to hit. Part 1 of 3.

Series: Strands Agents on Amazon Bedrock: Build, Understand and Ship a TypeScript AI Calendar Agent That Never Double-Books (3 articles)

  1. 1
    Build an AI Agent in 10 Minutes This article
Sr. Solutions Architect at GitLab | Ex-AWS | Golden Jacket Holder | Community Builder (AI)
Hey Builders!
Ask most agent demos to book two meetings at 3pm and they'll do it, then confirm both politely. The model called a tool, the tool ran, and your calendar is now wrong. The model isn't the problem. The problem is that "never double-book" only lived in the prompt.
In this series we build a calendar assistant with Strands Agents  for TypeScript and Claude on Amazon Bedrock . It books, moves, checks and cancels appointments in plain English. When a slot is taken, the tool refuses to save, tells the model what's in the way and which times are free, and the model asks you what to do. The code is on GitHub: schinchli/strands-calendar-agent  (Apache-2.0).
In this part
  • See the agent handle a real clash on Bedrock.
  • Set up Bedrock access with a least-privilege IAM policy and a one-command pre-flight check.
  • Clone, test and run the agent in about 10 minutes, with a checkpoint every few steps.
  • Fix the six errors you're most likely to hit.

See the Strands agent refuse a double-booking

Here's the whole flow, running live on Amazon Bedrock:
  1. Start. The header shows the model, region, calendar file and today's date.
  2. Book. "Book 'Agent fun' for tomorrow 3pm in NYC." The agent calls create_appointment and returns an ID.
  3. Move. "It's actually in DC." The agent reuses the ID and changes only the location.
  4. Clash. "Also book 'Design review' tomorrow at 3pm." The agent calls check_availability, names the clash, lists only free times and asks. Nothing is saved.
  5. Answer. "4pm works." Booked.
  6. Check. /calendar reads SQLite directly: two meetings, no overlap.
Ready: the header shows the model, region and calendar, and three sample requests are printed to copy
After setup, the header confirms the settings and prints the three requests used in the demo.
The agent calls check_availability, finds the clash with Agent fun at 3pm, and lists only the free slots before asking which to use
The clash turn: the agent checked availability, saved nothing, offered only free times, and waits for you.
Turn 1: the agent calls create_appointment with the date resolved from tomorrow 3pm and returns the new appointment ID
Turn 1: "tomorrow 3pm" becomes 2026-10-07 15:00, and the tool returns an ID.
Turn 2: the agent reuses the ID from turn 1 and calls update_appointment to change only the location
Turn 2: the agent reuses the ID and changes only the location.
Turn 4: after the user picks 4pm, create_appointment succeeds at 16:00
Turn 4: you choose, and only then does the booking happen.

Why I built it, and the mistake that shaped it

I started from the 02-custom-tools TypeScript sample in strands-agents/samples . It booked whatever the model asked for, and it no longer started, because it installed the SDK from an archived GitHub repository.
My first version put the rules in the system prompt. It worked most of the time. Then I ran the clash scenario repeatedly and read every transcript. Two failures kept coming back:
  • The model remembered the 3pm meeting from earlier in the chat, skipped the tools, and answered from memory.
  • When a tool returned the whole day's free slots, the model sometimes "helpfully" added a time that wasn't in the list, such as 2:30pm next to a 3–4pm meeting.
The fix was to make the tool enforce the rule. It checks overlaps in code, saves nothing on a clash, and returns exactly three free times.
I also made a mistake of my own. My first write-up said the agent "always asks before deleting". A trace showed the model calling delete_appointment with confirmed: true right after the user said yes. Correct behaviour, but the model sets that flag, so code can't guarantee it. I corrected the docs and the upstream pull request.
That's the idea the whole series rests on: a tool can return a decision, not just an error. Part 2 shows exactly how.

How Strands Agents call tools on Amazon Bedrock

Your code builds a Strands Agent with a Bedrock model and five tools, reads lines from the terminal, and calls agent.invoke() once per turn. Strands runs the loop: it sends the conversation and tool descriptions to the model, runs the tool the model picks, returns the result, and repeats until the model answers.
Architecture: the terminal app calls the Strands Agent, which calls Claude Haiku 4.5 on Amazon Bedrock and runs the five calendar tools against SQLite
  • create_appointment: Books a meeting unless it overlaps one. On a clash it saves nothing and returns the clash and three free times.
  • list_appointments: Lists everything booked.
  • update_appointment: Changes a meeting, with the same overlap check when the time moves.
  • check_availability: Lists free start times on a day, within working hours.
  • delete_appointment: Deletes only when called with confirmed: true.

Prerequisites: Node.js, AWS and Bedrock model access

  • Node.js 22 or later. The SDK requires it, and package.json declares "node": ">=22".
  • An AWS account with credentials in your shell (a named profile, SSO session or role). Avoid long-lived access keys where you can.
  • Amazon Bedrock access to Claude Haiku 4.5. Bedrock enables serverless models on first use if your identity has AWS Marketplace permissions. Anthropic models also need a one-time first-time-use form per account. See Access Amazon Bedrock foundation models .
  • The AWS CLI v2, for the pre-flight check.
Attach this policy to the identity that runs the agent. It covers the three Bedrock calls the Strands provider makes: ConverseStream (authorized by bedrock:InvokeModelWithResponseStream), Converse (bedrock:InvokeModel) and the optional CountTokens. A cross-region inference profile needs both the profile ARN and the foundation model ARN in each Region it routes to. See Prerequisites for inference profiles .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeClaudeHaiku45",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
"bedrock:CountTokens"
],
"Resource": [
"arn:aws:bedrock:*:<YOUR_ACCOUNT_ID>:inference-profile/us.anthropic.claude-haiku-4-5-20251001-v1:0",
"arn:aws:bedrock:*::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0"
]
}
]
}
We checked this policy with the IAM policy simulator: it allows the Haiku profile and the Haiku model in another US Region, and implicitly denies other models such as Claude Sonnet. If your organization requires it, replace the * Regions with the Regions the profile routes to.
Run this pre-flight check before you touch any code. It separates "my AWS setup is wrong" from "my code is wrong":
1
2
3
4
5
6
7
node --version # v22 or later
aws sts get-caller-identity --query Arn --output text
aws bedrock-runtime converse --region us-east-1 \
--model-id us.anthropic.claude-haiku-4-5-20251001-v1:0 \
--messages '[{"role":"user","content":[{"text":"Reply with the single word: ready"}]}]' \
--query 'output.message.content[0].text' --output text
# ready
If the last command prints ready, Bedrock, the model and your permissions are all in place.

Build the TypeScript AI agent in 10 steps

Step 1. Clone the repository.
1
2
git clone https://github.com/schinchli/strands-calendar-agent.git
cd strands-calendar-agent
Step 2. Install dependencies from the lockfile. On a clean machine this takes a few seconds.
1
npm ci
Step 3. Run the tests. They need no AWS credentials.
1
npm test
✅ Checkpoint: the summary shows ℹ pass 9 and ℹ fail 0. If npm ci printed EBADENGINE, switch to Node.js 22 or later before going further.
Step 4. Run the pre-flight check from Prerequisites and confirm it prints ready.
Step 5. See every option.
1
npm run dev -- --help
The help screen lists every mode, setting, environment variable and default
--help lists every mode, setting, environment variable and default.
Step 6. Choose your settings. Setup asks for each value, rejects invalid ones, and saves assistant.config.json.
1
npm run setup
✅ Checkpoint: cat assistant.config.json shows your Region, model and time zone, and git status does not list the file (it's in .gitignore).
Setup asks for each setting, shows the current value in brackets, and rejects an invalid date
Setup validates each answer and asks again when one is invalid.
Step 7. Run the guided demo. It sends the three sample requests, including the clash, then hands the chat to you.
1
npm run dev
Step 8. Answer the clash. When the agent offers free times, pick one, for example 4pm works.
Step 9. Check what was saved. Type /calendar, then /config.
✅ Checkpoint: /calendar lists exactly two appointments with no overlap, and each create_appointment call appears as a 🔧 Tool line in the transcript.
Step 10. Keep chatting. Try "What's on tomorrow?", "Move Design review to 5pm" or "Cancel Agent fun". Type exit to quit.
Try this. Push it. After booking 'Agent fun' at 3pm, ask: "Book a call tomorrow at 3pm, I don't mind overlapping", then "Yes, book it anyway". When I tried this, the model refused both times, and look closely: no 🔧 Tool line appeared. It refused from memory. This time the prompt did the work. That's fine, until the run where the model forgets. The booking tool is the backstop for that run, and Part 2 shows the three lines that make it one.

Fix common Amazon Bedrock and Strands errors

These are real errors I hit while building and testing, with the exact messages.
ModelError: Invocation of model ID anthropic.claude-haiku-4-5-20251001-v1:0 with on-demand throughput isn’t supported. Retry your request with the ID or ARN of an inference profile that contains this model.
You passed the base model ID. Claude Haiku 4.5 on demand is called through an inference profile: use us.anthropic.claude-haiku-4-5-20251001-v1:0 (or global.…).
ModelError: The provided model identifier is invalid.
The model ID is misspelled, or not offered in the Region you set. Check /config for the model and Region, and list what's available with aws bedrock list-inference-profiles --region <REGION>.
ModelError: The security token included in the request is invalid.
Your credentials are wrong or expired. Run aws sts get-caller-identity; for SSO, run aws sso login again.
Invalid settings: followed by a list, and the app exits with code 1
1
2
3
4
5
6
Invalid settings:
region "mars" does not look like an AWS region (e.g. us-east-1)
timezone "Mars/Base" is not a known IANA time zone (e.g. Asia/Kolkata, America/New_York)
duration "0" must be a whole number of minutes between 5 and 1440
workday "18-09" must be start-end hours, e.g. 09-18
Run with --help, or --setup to choose settings interactively.
Every problem is listed at once, before any Bedrock call. Fix the values, or run npm run setup.
Invalid settings are listed together and the app exits before calling Bedrock
npm warn EBADENGINE Unsupported engine
You are on Node.js 20 or earlier. Install Node.js 22 or later, then delete node_modules and run npm ci again. better-sqlite3 is a native module, so it must be rebuilt for the Node.js version you run it with.
Cannot find package …/@strands-agents/sdk/index.js
Your package.json installs Strands from the archived github:strands-agents/sdk-typescript. Depend on the npm package @strands-agents/sdk instead.

Clean up

Nothing runs in AWS after you exit, so clean-up is local plus IAM:
1
2
rm -f appointments.db assistant.config.json # calendar data and saved settings
cd .. && rm -rf strands-calendar-agent # the clone
If you created an IAM user or policy only for this walkthrough, detach and delete it, and delete any access keys you created. Model access in Bedrock has no standing charge, so you can leave it enabled.

What you learned

  • Strands keeps your code small. You build an Agent, give it tools, and call invoke() once per turn. Strands runs the loop.
  • Check the AWS side first. A pre-flight converse call separates IAM and model-access problems from code problems.
  • Prompts guide; tools enforce. The model usually follows the prompt. When it doesn't, the booking tool still refuses the clash, and that's the guarantee you can rely on.

Up next

You've seen what it does. In Part 2: Tools the model can't break, we open the nine pieces of code that make it work. You'll see the one comparison that defines a clash, the small JSON report that turns a refusal into a question, and the honest limit of the delete tool.

Series: Strands Agents on Amazon Bedrock: Build, Understand and Ship a TypeScript AI Calendar Agent That Never Double-Books (3 articles)

  1. 1
    Build an AI Agent in 10 Minutes This article
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article