Back to Writing
March 24, 2026

Building a Personal AI Assistant on Telegram with n8n — Emails, Calendar, Contacts and Calls

#n8n#OpenAI#Workflow Automation#Perplexity AI#VAPI#Twilio#Prompt Engineering#Telegram Bot#AI Automation
Building a Personal AI Assistant on Telegram with n8n — Emails, Calendar, Contacts and Calls

How I Built a Personal AI Assistant on Telegram That Manages My Emails, Calendar, Contacts and Makes Phone Calls

So I wanted a personal AI assistant that actually does things — not just chats. No switching between apps, no copy-pasting. Just message it on Telegram and it handles everything. Here's exactly how I built it, the mistakes I made, and the prompts that actually work.


What It Can Do

By the end of this build, my assistant could:

  • Read, send, draft and reply to emails via Gmail

  • Create, update, delete and check calendar events via Google Calendar

  • Add, search and manage contacts via Google Contacts

  • Search the web in real time via Perplexity

  • Make phone calls via VAPI + Twilio

All triggered from a single Telegram message. One workflow, five agents.


The Architecture

The idea is simple — one Master AI Agent sits at the top. It reads what you say, decides which specialist agent to call, and returns the result. Each specialist agent has its own set of tools connected to it.

Telegram → Master AI Agent
               ├── EMAIL_AGENT_TOOL
               │      ├── GMAIL_SEND
               │      ├── GMAIL_DRAFT
               │      ├── GMAIL_REPLY
               │      ├── GMAIL_LIST
               │      └── GMAIL_GET
               ├── CALENDAR_AGENT_TOOL
               │      ├── GCAL_CREATE
               │      ├── GCAL_GET
               │      ├── GCAL_LIST
               │      ├── GCAL_UPDATE
               │      └── GCAL_DELETE
               ├── CONTACT_MANAGER_AGENT_TOOL
               │      ├── CONTACTS_GET
               │      ├── CONTACTS_ADD
               │      ├── CONTACTS_UPDATE
               │      └── CONTACTS_DELETE
               ├── CALLING_AGENT_TOOL
               │      └── VAPI calling tool (HTTP Request)
               └── Perplexity (direct tool)

Everything is built in n8n — a no-code workflow automation tool.


Tools Used

  • n8n — workflow builder

  • OpenAI GPT-4o — the brain for all agents

  • Gmail nodes — email operations

  • Google Calendar nodes — calendar operations

  • Google Contacts nodes — contact management

  • Perplexity API — real-time web search

  • VAPI — AI phone calling

  • Twilio — phone number for outbound calls

  • Telegram Bot — the interface


Step 1 — The Master AI Agent

This is the orchestrator. Its only job is to route requests to the right specialist. I kept the prompt intentionally short because I learned that longer prompts with too many rules actually confuse the model and give it more reasons not to call a tool.

Master Agent System Prompt:

You are an orchestrator. You ONLY answer by calling tools. Never answer from your own knowledge.

Available tools:
- EMAIL_AGENT_TOOL → email, inbox, messages, mails
- CALENDAR_AGENT_TOOL → calendar, events, meetings, schedules, reminders
- CONTACT_MANAGER_AGENT_TOOL → contacts, phone numbers, saving or finding people
- CALLING_AGENT_TOOL → making phone calls, calling someone, call this number
- Message a model in Perplexity → web search, real-time info, recommendations, current events

Rules:
- Always call a tool. Never skip.
- Pass the user's request to the right tool exactly as intended.
- Return the tool's response as-is.
- Only say "no tool available" if the request has absolutely nothing to do with any listed tool.

Mistake I made: My first prompt was huge — keyword lists, fallback rules, domain examples. The agent kept saying "I don't have a tool for that" even for clear email requests like "summarise my top 5 mails." Stripping it down to the essentials fixed it immediately.


Step 2 — Email Agent

The Email Agent handles everything Gmail. It has five tools connected to it.

Node naming (important — the agent uses these names to decide which tool to call):

n8n Node Rename to Send a message in Gmail GMAIL_SEND Create a draft in Gmail GMAIL_DRAFT Reply to a message in Gmail GMAIL_REPLY Get many messages in Gmail GMAIL_LIST Get a message in Gmail GMAIL_GET

Email Agent System Prompt:

You are the Email AI Agent. You ONLY answer by interacting with Gmail tools. Never answer from your own knowledge or memory.

Today's date and time is: {{ $now.setZone('Asia/Kolkata').toFormat('yyyy-MM-dd HH:mm') }} IST
When the user says "yesterday", "today" etc., use full calendar days, never rolling 24-hour windows.

Available tools and when to use them:
- GMAIL_LIST → search or find emails
- GMAIL_GET → read full content of a specific email (use after GMAIL_LIST)
- GMAIL_SEND → send a new email
- GMAIL_DRAFT → save as draft without sending
- GMAIL_REPLY → reply to an existing thread (get thread ID from GMAIL_LIST first)

Action rules:
- "send" → send immediately, confirm after
- "draft" → draft immediately, confirm after
- "reply" → reply immediately, confirm after
- Only pause and ask if critical information is truly missing
- Never ask "are you sure?" before acting
- All emails are sent from 'kreatenvibe'. Sign off with "Regards, Kreatenvibe" unless told otherwise

Multi-step workflow:
- Reading → GMAIL_LIST first → GMAIL_GET for content
- Replying → GMAIL_LIST to find thread → GMAIL_REPLY with thread ID

Mistake I made: I originally set the user prompt field to "Define below" but left it empty, which caused the agent to throw a "no prompt specified" error. You need to wire the Telegram message into the user prompt field using {{ $('Telegram Trigger').item.json.message.text }}.

Another issue: When I asked for "yesterday's emails" it kept missing one email. The problem was passing {{ $now }} as the date — it includes the current time, so "yesterday" became exactly 24 hours ago and cut off emails sent earlier in the day. Passing only the date string {{ $now.setZone('Asia/Kolkata').toFormat('yyyy-MM-dd') }} fixed it.


Step 3 — Calendar Agent

Node naming:

n8n Node Rename to Create an event in Google Calendar GCAL_CREATE Get an event in Google Calendar GCAL_GET Get many events in Google Calendar GCAL_LIST Update an event in Google Calendar GCAL_UPDATE Delete an event in Google Calendar GCAL_DELETE

Calendar Agent System Prompt:

You are the Calendar AI Agent. You ONLY interact with Google Calendar tools. Never answer from your own knowledge or memory.

Today's date and time is: {{ $now.setZone('Asia/Kolkata').toFormat('yyyy-MM-dd HH:mm') }} IST (UTC+5:30)
All events must be created, updated, and displayed in IST timezone unless the user specifies otherwise.
When the user says "yesterday", "tomorrow", "next week" etc., calculate from the above date using full calendar days.

Available tools:
- GCAL_CREATE → create a new event
- GCAL_GET → fetch one specific event
- GCAL_LIST → fetch multiple events (use for "what's on my calendar", "any meetings today")
- GCAL_UPDATE → modify an existing event
- GCAL_DELETE → remove an event

Action rules:
- Create, update, delete → execute immediately, confirm after
- If duration is not mentioned → assume 1 hour by default
- Always call GCAL_LIST first to find an event before updating or deleting it
- Never ask "are you sure?" before acting

Response format:
- Reads: event name, date, time, details
- Writes: confirm what was created, changed, or deleted

Step 4 — Contact Manager Agent

I originally tried Google Sheets for contacts but ran into two big problems — the + in phone numbers caused formula parse errors, and the agent kept adding duplicates even after I added duplicate detection logic to the prompt. I eventually switched to Google Contacts which is purpose built for this.

Node naming:

n8n Node Rename to Get many contacts CONTACTS_GET Create a contact CONTACTS_ADD Update a contact CONTACTS_UPDATE Delete a contact CONTACTS_DELETE

Important config: In the CONTACTS_GET node, you must select fields — Names, Phone Numbers, Email Addresses — otherwise it throws a personFields mask is required error.

Contact Manager System Prompt:

You are the Contact Manager AI Agent. You ONLY interact with Google Contacts tools. Never answer from your own knowledge or memory.

Available tools:
- CONTACTS_GET → fetch all contacts
- CONTACTS_ADD → add new contact
- CONTACTS_UPDATE → update existing contact
- CONTACTS_DELETE → delete a contact

═══ PHONE NUMBER HANDLING ═══
- Always store phone with country code (e.g. +919876543210)
- If no country code given → default to +91
- When comparing for duplicates → compare full number including country code

═══ ADDING A CONTACT ═══
1. Call CONTACTS_GET
2. Compare user's number against all existing phone numbers
3. If match found → tell user "This number belongs to [Name] already. Update instead?"
4. If similar name found → ask "Is this the same person as [Name]?"
5. Only call CONTACTS_ADD if no duplicates found

═══ DELETING A CONTACT ═══
1. Call CONTACTS_GET to find the contact
2. Confirm with user: "Are you sure you want to delete [Name]?"
3. Only call CONTACTS_DELETE after user confirms

═══ REQUIRED ═══
- Name and Phone are mandatory — ask if missing
- Email and Notes are optional — skip if not provided

═══ RESPONSE FORMAT ═══
- Reads: Name, Phone, Email
- Writes: confirm exactly what was added, updated or deleted

Mistake I made: I had separate Country Code and Phone columns in Sheets. Google Sheets was storing +91 as a number 91 because it stripped the +. This caused the duplicate check to fail since +91 9876543210 never matched 91 + 9876543210. Moving to Google Contacts eliminated this entirely.


Step 5 — Perplexity Web Search

I tried wrapping Perplexity inside an AI Agent Tool node with OpenAI as the brain — it kept throwing The Tool attempted to return an engine request, which is not supported in Agents. You cannot use an LLM as a tool inside another agent in n8n.

The fix is simple — connect Perplexity directly to the Master Agent's Tool slot, bypassing the sub-agent wrapper entirely. Then click the ✨ button on the Messages field so the Master Agent can dynamically pass the user's query into it.

Perplexity System Prompt (inside the node):

Today's date and time is: {{ $now.setZone('Asia/Kolkata').toFormat('yyyy-MM-dd HH:mm') }} IST

That's it. Perplexity already knows how to search — over-instructing it only limits what it can do.


Step 6 — Calling Agent (VAPI + Twilio)

For phone calls I used VAPI with a Twilio number. The Calling Agent is a sub-agent with an HTTP Request node connected as its tool.

HTTP Request node config:

  • Method: POST

  • URL: https://api.vapi.ai/call

  • Headers: Authorization: Bearer YOUR_VAPI_API_KEY and Content-Type: application/json

  • Body (JSON):

{
  "assistantId": "your-vapi-assistant-id",
  "phoneNumberId": "your-vapi-phone-number-id",
  "phoneNumber": {
    "twilioAccountSid": "your-twilio-sid",
    "twilioPhoneNumber": "+1xxxxxxxxxx"
  },
  "assistantOverrides": {
    "variableValues": {
      "name": "",
      "mission": ""
    }
  },
  "customer": {
    "number": "+91xxxxxxxxxx"
  }
}

Note: phoneNumberId is NOT the Twilio SID. It's VAPI's internal ID for your number — find it in VAPI Dashboard → Phone Numbers → click your number.

Calling Agent System Prompt:

You are the Calling AI Agent. You ONLY trigger calls via the VAPI calling tool.

When the user wants to call someone:
1. Extract the phone number and purpose of the call
2. If phone number is missing → ask before proceeding
3. Send the JSON payload to the VAPI calling tool with:
   - name: recipient name if provided, else "there"
   - mission: purpose of the call from the user's request
   - customer.number: phone number with country code (default +91)

Rules:
- Never make up a mission — use exactly what the user described
- Confirm after triggering: "Call initiated to [number] for [mission]"

VAPI Assistant System Prompt (inside VAPI dashboard):

You are a smart AI calling assistant from Kreatenvibe.
Your mission for this call: {{mission}}
You are calling: {{name}}

Rules:
- Greet the person by name if available
- Be polite, concise and professional
- Stay focused on the mission — do not go off topic
- If the person is unavailable, apologize and end the call
- If asked who you are, say you are an AI assistant calling on behalf of Kreatenvibe
- End the call politely once the mission is complete

Key Lessons

1. Shorter prompts work better for routing. The more rules you add about when NOT to call a tool, the more reasons the model finds to avoid calling it. Keep the master agent prompt minimal.

2. Always name your n8n nodes clearly. The agent decides which tool to call based on the node name and description. Vague names like "Google Sheets 2" will confuse it.

3. Pass date as a string, not a timestamp. {{ $now }} includes time and causes relative date calculations to use rolling windows instead of calendar days.

4. Isolate issues before fixing prompts. When contacts weren't being fetched, I built a separate test workflow with just a Manual Trigger and the Google Contacts node. That's how I found it was a personFields config issue, not a prompt issue.

5. Don't nest LLMs as tools inside agents. Perplexity, GPT, Claude — these are engines, not tools. Plug them into the Chat Model slot or call them via HTTP Request, not the Tool slot.

6. Use purpose-built integrations. Google Contacts is infinitely cleaner than a spreadsheet for contact management. Use the right tool for the job.


What's Next

This is a living workflow. I plan to add:

  • Slack messaging agent

  • Task manager agent (Notion or Todoist)

  • Voice-to-text input via Telegram voice messages

If you build something similar I'd love to see it. The workflow file will be linked below once I clean it up for sharing.

Building a Personal AI Assistant on Telegram with n8n — Emails, Calendar, Contacts and Calls