An AI agent that schedules calendar events using natural language. Built with Node.js + React. Works with Ollama (local) or Anthropic Claude (production).
# 1. Pull model (Ollama)
ollama pull qwen2.5-coder:7b
ollama serve
# 2. Server
cd server && npm install
cp .env.example .env
npm run dev # → http://localhost:3001
# 3. Client
cd client && npm install
npm run dev # → http://localhost:5173"Schedule a team standup tomorrow at 10am to 10:30am"
You are a calendar scheduling assistant.
Today's date is 2026-07-25.
When a user wants to schedule a meeting, you MUST:
1. Extract the EXACT date, start_time, end_time and title
2. Call check_availability with REAL extracted values
3. If available, call schedule_event with the same REAL values
4. Confirm success with a friendly message
IMPORTANT:
- Always use real dates in YYYY-MM-DD format (e.g. 2026-07-26)
- Always use real times in HH:MM format (e.g. 10:00)
- If user says "tomorrow", calculate from today: 2026-07-25
- Never pass placeholder values like "YYYY-MM-DD" or "00:00"
[
{
"name": "check_availability",
"description": "Check if a time slot is available on the calendar",
"input_schema": {
"type": "object",
"properties": {
"date": { "type": "string", "description": "Date in YYYY-MM-DD" },
"start_time": { "type": "string", "description": "Start time HH:MM" },
"end_time": { "type": "string", "description": "End time HH:MM" }
},
"required": ["date", "start_time", "end_time"]
}
},
{
"name": "schedule_event",
"description": "Schedule a calendar event after availability confirmed",
"input_schema": {
"type": "object",
"properties": {
"title": { "type": "string" },
"date": { "type": "string" },
"start_time": { "type": "string" },
"end_time": { "type": "string" },
"attendees": { "type": "array", "items": { "type": "string" } }
},
"required": ["title", "date", "start_time", "end_time"]
}
}
]What we send to the LLM:
{
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"system": "...system prompt above...",
"tools": [...tool definitions above...],
"messages": [
{
"role": "user",
"content": "Schedule a team standup tomorrow at 10am to 10:30am"
}
]
}LLM decides to check availability first:
{
"stop_reason": "tool_use",
"content": [
{
"type": "text",
"text": "I'll check if that time slot is available for you."
},
{
"type": "tool_use",
"id": "tool_abc123",
"name": "check_availability",
"input": {
"date": "2026-07-26",
"start_time": "10:00",
"end_time": "10:30"
}
}
]
}stop_reason: "tool_use" → loop continues
Your code executes the tool (not the LLM):
Input:
date : "2026-07-26"
start_time : "10:00"
end_time : "10:30"
Result:
available : true
slot : "10:00–10:30"
date : "2026-07-26"
message : "Time slot is available"
We send the full conversation history back to LLM:
{
"model": "claude-sonnet-4-6",
"system": "...same system prompt...",
"tools": [...same tools...],
"messages": [
{
"role": "user",
"content": "Schedule a team standup tomorrow at 10am to 10:30am"
},
{
"role": "assistant",
"content": [
{ "type": "text", "text": "I'll check if that time slot is available." },
{ "type": "tool_use", "id": "tool_abc123",
"name": "check_availability",
"input": { "date": "2026-07-26", "start_time": "10:00", "end_time": "10:30" }
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "tool_abc123",
"content": "{\"available\":true,\"slot\":\"10:00–10:30\",\"date\":\"2026-07-26\"}"
}
]
}
]
}LLM sees slot is available, decides to book it:
{
"stop_reason": "tool_use",
"content": [
{
"type": "tool_use",
"id": "tool_def456",
"name": "schedule_event",
"input": {
"title": "Team Standup",
"date": "2026-07-26",
"start_time": "10:00",
"end_time": "10:30",
"attendees": []
}
}
]
}stop_reason: "tool_use" → loop continues
Your code executes the tool:
Input:
title : "Team Standup"
date : "2026-07-26"
start_time : "10:00"
end_time : "10:30"
attendees : []
Result:
success : true
event_id : "EVT-1753401600000"
title : "Team Standup"
date : "2026-07-26"
time : "10:00–10:30"
calendar_link : "https://calendar.example.com/event/EVT-1753401600000"
Full conversation history sent again — now with both tool results:
{
"model": "claude-sonnet-4-6",
"system": "...same system prompt...",
"tools": [...same tools...],
"messages": [
{ "role": "user", "content": "Schedule a team standup tomorrow at 10am to 10:30am" },
{ "role": "assistant", "content": [ ...check_availability tool_use... ] },
{ "role": "user", "content": [ ...check_availability tool_result... ] },
{ "role": "assistant", "content": [ ...schedule_event tool_use... ] },
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "tool_def456",
"content": "{\"success\":true,\"event_id\":\"EVT-1753401600000\",\"title\":\"Team Standup\"}"
}
]
}
]
}LLM sees both tools succeeded, generates friendly confirmation:
{
"stop_reason": "end_turn",
"content": [
{
"type": "text",
"text": "Your Team Standup has been scheduled! ✅\n\n📅 Date : Sunday, July 26, 2026\n🕙 Time : 10:00 AM – 10:30 AM\n🔗 Link : https://calendar.example.com/event/EVT-1753401600000\n\nLet me know if you need any changes!"
}
]
}stop_reason: "end_turn" → loop exits → response shown to user
┌─────────────────────────────────────────────────────────┐
│ USER INPUT │
│ "Schedule standup tomorrow at 10am" │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ SYSTEM PROMPT + TOOLS + MESSAGE │
│ → sent to LLM │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ LLM TURN 1 RESPONSE │
│ stop_reason: "tool_use" │
│ → tool: check_availability │
│ → input: { date, start_time, end_time } │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ YOUR CODE EXECUTES TOOL │
│ check_availability() │
│ → result: { available: true } │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ FULL HISTORY + TOOL RESULT → sent to LLM │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ LLM TURN 2 RESPONSE │
│ stop_reason: "tool_use" │
│ → tool: schedule_event │
│ → input: { title, date, start_time, end_time }│
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ YOUR CODE EXECUTES TOOL │
│ schedule_event() │
│ → result: { success: true, event_id } │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ FULL HISTORY + TOOL RESULT → sent to LLM │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ LLM TURN 3 RESPONSE │
│ stop_reason: "end_turn" ← LOOP EXITS │
│ → "Your standup is booked! ✅" │
└─────────────────────────────────────────────────────────┘
| Concept | Explanation |
|---|---|
stop_reason: "tool_use" |
LLM wants to call a tool — loop continues |
stop_reason: "end_turn" |
LLM is done — loop exits, show final response |
messages[] |
Grows every turn — this IS the agent's memory |
| System prompt | Sent every turn unchanged — defines agent behavior |
| Tools | Sent every turn unchanged — LLM picks when to call |
| executeTool() | YOUR code calls real APIs — LLM never calls directly |
calendar-agent/
├── server/
│ ├── index.js # Express server (port 3001)
│ ├── agent.js # Agentic loop + provider adapter
│ ├── logger.js # Session logger → logs/*.txt
│ └── .env.example # Copy to .env
└── client/
└── src/
└── App.jsx # React UI — shows each agent step
# server/.env
LLM_PROVIDER=ollama # local dev (default)
LLM_PROVIDER=anthropic # production
ANTHROPIC_API_KEY=sk-ant-xxxx
OLLAMA_MODEL=qwen2.5-coder:7bNo code changes needed — one env variable switches everything.
Every session creates a log file in server/logs/:
logs/session-2026-07-25T10-30-00.txt
Contains: system prompt, user input, every LLM request, every LLM decision, every tool call, every tool result, and the final response — in order.