An AI voice receptionist that schedules meetings via phone. Callers speak to an agent (powered by Smallest.ai Atoms), which checks your Google Calendar availability and books meetings through this API.
- Node.js 18+ and npm
- Google Cloud account (for Calendar API)
- Smallest.ai account (for Atoms voice agent)
- ngrok (free tier) for local development
Quick path (Option A – duplicate the agent): Clone → Install server + client → Google Cloud → Create server/.env → Run server + ngrok → Run npm run setup-atoms -- <ngrok-url> → Add agent ID to both .env files → Test. Skip Step 6.
git clone https://github.com/malikaa-27/calendarAI.git
cd calendarAIcd server && npm install
cd ../client && npm install- Go to Google Cloud Console
- Create or select a project
- APIs & Services → Enable APIs → enable Google Calendar API
- APIs & Services → Credentials → Create Credentials → Service Account
- Name it (e.g.
calendar-receptionist) and create - On the service account, go to Keys → Add Key → Create new key → JSON
- Download the JSON file
Option A – Same Google account as service account
- Use the service account’s own calendar (less common)
Option B – Impersonate a user (recommended)
- In Google Workspace Admin: Security → Access and data control → API Controls
- Add the service account’s Client ID (from the JSON) and grant the scope:
https://www.googleapis.com/auth/calendar - In your
.env, setGCP_IMPERSONATE=trueandGCP_SUBJECT_EMAILto the calendar owner’s email
From the downloaded JSON:
client_email→GCP_CLIENT_EMAILprivate_key→GCP_PRIVATE_KEY(keep the\nnewlines as literal\nin .env)project_id→GCP_PROJECT_ID
Option A – Duplicate the agent (recommended)
- Sign up at Smallest.ai and get your API Key
- Add
SMALLEST_API_KEYtoserver/.env(Step 4) - After Step 5 (once you have your ngrok URL), run:
Replace
npm run setup-atoms -- https://YOUR-NGROK-URL
https://YOUR-NGROK-URLwith your actual ngrok URL (e.g.https://abc123.ngrok-free.app). - The script outputs a new agent ID. Add it to both env files:
server/.env:SMALLEST_RECEPTIONIST_AGENT_ID=<agent-id>client/.env: Create fromclient/.env.exampleand setVITE_SMALLEST_ASSISTANT_ID=<agent-id>
- Skip Step 6 – the workflow is already configured.
Option B – Manual
- Sign up at Smallest.ai
- Create an Atoms agent (workflow or conversational)
- Note your Agent ID and API Key for
.env - Configure the API calls in Atoms (Step 6)
cd server
cp .env.example .envEdit server/.env:
| Variable | Required | Description |
|---|---|---|
SMALLEST_API_KEY |
Yes | Your Smallest.ai API key |
SMALLEST_API_BASE_URL |
Yes | https://atoms-api.smallest.ai |
SMALLEST_RECEPTIONIST_AGENT_ID |
Yes | Your Atoms agent ID |
GCP_CLIENT_EMAIL |
Yes | Service account email from JSON |
GCP_PRIVATE_KEY |
Yes | Private key from JSON (use \n for newlines) |
GCP_PROJECT_ID |
Yes | Google Cloud project ID |
GCP_SUBJECT_EMAIL |
Yes | Email of the calendar to use |
GCP_IMPERSONATE |
Optional | true if using domain-wide delegation |
SMTP_HOST |
Optional | e.g. smtp.gmail.com (for confirmation emails) |
SMTP_USER |
Optional | Your email |
SMTP_PASS |
Optional | App password (Gmail: use App Password) |
EMAIL_FROM |
Optional | e.g. "Calendar Receptionist <you@gmail.com>" |
Client: Create client/.env from the example and add your agent ID (from Step 3 Option A, or from Atoms if Option B):
cp client/.env.example client/.env
# Edit client/.env: VITE_SMALLEST_ASSISTANT_ID=<your-agent-id>cd server
npm run devYou should see: Server listening on port 4000
ngrok http 4000Copy the HTTPS URL (e.g. https://abc123.ngrok-free.app). This is your webhook base URL.
Skip this step if you used Option A. The setup script already configured the workflow, APIs, and prompt.
If you created your agent manually (Option B), configure it in the Smallest.ai Atoms dashboard as follows.
Add this parameter so the caller’s requested day/time is sent to the API:
| Parameter name | day_time_mentioned_by_user |
|---|---|
| Description | "The day and time the caller wants. Extract exactly what they said. Examples: today 2 pm, tomorrow, Friday 3 pm, next Monday, March 15, March 15 2026, March 15th next year, 12/25/2026. Any day - this week, next year, or specific date." |
| Field | Value |
|---|---|
| Name | getAvailableSlots |
| URL | https://YOUR-NGROK-URL/webhooks/check-availability |
| Method | POST |
| Headers | Content-Type: application/jsonngrok-skip-browser-warning: true |
| Body | {"proposedSlots":[],"targetDay":"{{day_time_mentioned_by_user}}"} |
| LLM Parameter | day_time_mentioned_by_user (from 6.1) |
| Timeout | 10000 |
Response Variable Extraction (add exactly these):
| Variable | Path |
|---|---|
available_summary |
$.available_summary |
selected_slot_start_iso |
$.first_slot_start |
selected_slot_end_iso |
$.first_slot_end |
Important: Do not add $.available[0].start, $.available[0].end, $.formatted, or $.slots – they cause errors. Use $.first_slot_start and $.first_slot_end only. Variable name must be exactly selected_slot_end_iso (no trailing space).
| Field | Value |
|---|---|
| Name | confirmMeeting |
| URL | https://YOUR-NGROK-URL/webhooks/confirm-meeting |
| Method | POST |
| Headers | Content-Type: application/jsonngrok-skip-browser-warning: true |
| Body | {"start":"{{selected_slot_start_iso}}","end":"{{selected_slot_end_iso}}","clientEmail":"{{client_email}}","purpose":"{{meeting_purpose}}","attendeeName":"{{caller_name}}"} |
| Timeout | 10000 |
Response Variable Extraction:
| Variable | Path |
|---|---|
confirmation_message |
$.confirmationMessage |
Do not add $.ok or $.event – they fail on error responses.
In your agent’s system prompt, include logic that:
- Asks for the caller’s preferred day/time
- Calls getAvailableSlots with
{{day_time_mentioned_by_user}} - Reads back times using
{{available_summary}} - Collects name, email, and meeting purpose
- Calls confirmMeeting with the selected slot
- On success, says
{{confirmation_message}}
Use the full prompt in AGENT_PROMPT.txt as a template. Customize the name (e.g. replace "Malikaa" with your name).
Ensure your agent extracts and passes:
client_email– caller’s emailcaller_name– caller’s namemeeting_purpose– purpose of the meeting
These are typically collected via conversation nodes and stored as variables.
- Terminal 1:
cd server && npm run dev - Terminal 2:
ngrok http 4000 - Terminal 3:
cd client && npm run dev - Open the client in your browser, or trigger a test call from the Atoms dashboard
- Say you want to schedule a meeting (e.g. "I'd like to book for tomorrow at 2 pm")
- Provide name, email, and purpose when asked
- Confirm the booking
Check ngrok at http://127.0.0.1:4040 to inspect requests and responses.
| Issue | Fix |
|---|---|
| "Not getting backend calendar info" | Verify ngrok URL is correct, ngrok-skip-browser-warning: true is set, and Response Variable Extraction is configured |
| "NoneType" or extraction errors | Remove $.available[0].start, $.formatted, $.ok, $.event – use only the paths in Step 6 |
| Wrong day/time offered | Ensure day_time_mentioned_by_user LLM parameter exists and is passed to getAvailableSlots |
| "Meeting end must be after start" | Check selected_slot_end_iso has no trailing space in the variable name |
| Emails not sent | Configure SMTP in .env; for Gmail use an App Password |
| "Time range is too long" | Backend chunks freebusy queries automatically; restart the server |
| ngrok URL changed | Update both API URLs in Atoms |
calendarAI/
├── client/ # React web app with Atoms widget
│ ├── src/App.tsx
│ └── .env # VITE_SMALLEST_ASSISTANT_ID
├── server/
│ ├── src/
│ │ ├── controllers/webhookController.ts # Availability & confirm-meeting logic
│ │ ├── services/googleCalendarService.ts # Calendar API
│ │ └── services/emailService.ts # Confirmation emails
│ └── .env # Your secrets (not in git)
├── scripts/ # Export, setup, list-agents (see scripts/README.md)
├── atoms-agent-config.json # Agent + workflow for duplication
├── AGENT_PROMPT.txt # Full prompt template (Option B)
├── ATOMS_CONFIG_NOW.txt # Quick reference for Atoms config
└── README.md # This file
| Endpoint | Method | Purpose |
|---|---|---|
/webhooks/check-availability |
POST | Get available slots for a given day |
/webhooks/confirm-meeting |
POST | Book a meeting and send confirmation |
See server/docs/webhook_examples.md for request/response examples.
MIT