How to Build a Meeting Scheduler AI Agent

Learn how to build a meeting scheduler AI agent using LangChain and the Google Calendar API. This step-by-step tutorial covers OAuth setup, availability checking, event creation, and automated email confirmations.

Person scheduling consultations on a computer using Google Calendar
Photo by Gaining Visuals on Unsplash
Computer screen showing a calendar booking interface
Photo by Ed Hardie on Unsplash

Scheduling meetings is one of the most time-consuming administrative tasks in any organization. The back-and-forth of finding a mutually available slot, sending calendar invites, and following up with confirmations can consume 30 minutes or more per meeting. A meeting scheduler AI agent eliminates that friction entirely — it reads availability, proposes times, creates events, and sends confirmation emails autonomously.

In this tutorial, you will build a fully functional meeting scheduler AI agent using LangChain and the Google Calendar API. The agent understands natural language requests like "schedule a 45-minute call with Sarah next Tuesday afternoon" and handles the entire workflow without human intervention.

Prerequisites#

Before starting, make sure you have the following:

  • Python 3.10 or later
  • A Google Cloud project with the Calendar API enabled
  • Google OAuth 2.0 credentials (client ID and client secret)
  • An OpenAI API key (or another LangChain-compatible LLM)
  • Basic familiarity with Python and REST APIs

Install the required packages:

pip install langchain langchain-openai google-auth google-auth-oauthlib google-api-python-client python-dotenv

Architecture Overview#

The meeting scheduler agent uses a tool-calling architecture. The LLM acts as the reasoning layer — it interprets the user's intent and decides which tools to invoke. The tools are thin wrappers around the Google Calendar API.

The core components are:

  1. LLM (GPT-4o) — Parses intent, resolves ambiguous time references, and decides action sequence
  2. check_availability tool — Queries free/busy data from Google Calendar
  3. create_event tool — Creates a calendar event and sends invitations
  4. send_confirmation_email tool — Sends a plain-text confirmation via Gmail API
  5. LangChain AgentExecutor — Orchestrates the tool loop

The agent follows a standard ReAct (Reason + Act) loop: it reasons about the request, calls a tool, observes the result, and either calls another tool or returns a final answer.

Step 1: Set Up Google Calendar OAuth#

First, enable the Google Calendar API in your Google Cloud Console, then download your credentials.json file. The following helper handles the OAuth token flow and persists the token for reuse.

# auth/google_auth.py
import os
import json
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow

SCOPES = [
    "https://www.googleapis.com/auth/calendar",
    "https://www.googleapis.com/auth/gmail.send",
]

def get_google_credentials(token_path: str = "token.json",
                            creds_path: str = "credentials.json") -> Credentials:
    """Load or refresh Google OAuth credentials."""
    creds = None

    if os.path.exists(token_path):
        creds = Credentials.from_authorized_user_file(token_path, SCOPES)

    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(creds_path, SCOPES)
            creds = flow.run_local_server(port=0)

        with open(token_path, "w") as token:
            token.write(creds.to_json())

    return creds

Run this once interactively to authorize your account. The token is saved and reused on subsequent runs.

Step 2: Build the check_availability Tool#

The availability tool takes a list of email addresses and a time range, then queries the Google Calendar free/busy API. It returns a structured summary of when each attendee is free.

# tools/availability.py
from datetime import datetime, timezone
from googleapiclient.discovery import build
from langchain.tools import tool
from auth.google_auth import get_google_credentials

def get_calendar_service():
    creds = get_google_credentials()
    return build("calendar", "v3", credentials=creds)

@tool
def check_availability(
    attendee_emails: list[str],
    start_time: str,
    end_time: str
) -> str:
    """
    Check calendar availability for a list of attendees within a time range.

    Args:
        attendee_emails: List of email addresses to check.
        start_time: ISO 8601 start datetime (e.g., '2026-03-10T09:00:00-05:00').
        end_time: ISO 8601 end datetime (e.g., '2026-03-10T17:00:00-05:00').

    Returns:
        A plain-text summary of free and busy slots.
    """
    service = get_calendar_service()

    body = {
        "timeMin": start_time,
        "timeMax": end_time,
        "items": [{"id": email} for email in attendee_emails],
    }

    result = service.freebusy().query(body=body).execute()
    calendars = result.get("calendars", {})

    lines = []
    for email in attendee_emails:
        busy_slots = calendars.get(email, {}).get("busy", [])
        if not busy_slots:
            lines.append(f"{email}: Completely free during this window.")
        else:
            formatted = [f"  - {s['start']} to {s['end']}" for s in busy_slots]
            lines.append(f"{email} is busy at:\n" + "\n".join(formatted))

    return "\n\n".join(lines)

The @tool decorator from LangChain automatically generates the JSON schema that the LLM uses to call this function. The docstring becomes the tool description — keep it accurate and specific.

Step 3: Build the create_event Tool#

Once the agent identifies a free slot, it calls this tool to create the calendar event and send invites to all attendees.

# tools/create_event.py
from googleapiclient.discovery import build
from langchain.tools import tool
from auth.google_auth import get_google_credentials

@tool
def create_event(
    title: str,
    start_time: str,
    end_time: str,
    attendee_emails: list[str],
    description: str = "",
    timezone: str = "America/New_York"
) -> str:
    """
    Create a Google Calendar event and send invitations.

    Args:
        title: Event title (e.g., 'Q1 Strategy Review').
        start_time: ISO 8601 start datetime.
        end_time: ISO 8601 end datetime.
        attendee_emails: List of attendee email addresses.
        description: Optional event description or agenda.
        timezone: IANA timezone string (default: America/New_York).

    Returns:
        A confirmation string with the event link.
    """
    creds = get_google_credentials()
    service = build("calendar", "v3", credentials=creds)

    event_body = {
        "summary": title,
        "description": description,
        "start": {"dateTime": start_time, "timeZone": timezone},
        "end": {"dateTime": end_time, "timeZone": timezone},
        "attendees": [{"email": e} for e in attendee_emails],
        "reminders": {
            "useDefault": False,
            "overrides": [
                {"method": "email", "minutes": 24 * 60},
                {"method": "popup", "minutes": 30},
            ],
        },
        "conferenceData": {
            "createRequest": {
                "requestId": f"meet-{hash(title + start_time)}",
                "conferenceSolutionKey": {"type": "hangoutsMeet"},
            }
        },
    }

    event = service.events().insert(
        calendarId="primary",
        body=event_body,
        conferenceDataVersion=1,
        sendUpdates="all",
    ).execute()

    link = event.get("htmlLink", "")
    meet_link = event.get("conferenceData", {}).get("entryPoints", [{}])[0].get("uri", "")

    return (
        f"Event created successfully!\n"
        f"Title: {title}\n"
        f"Start: {start_time}\n"
        f"Calendar link: {link}\n"
        f"Google Meet link: {meet_link}"
    )

The conferenceData block automatically generates a Google Meet link — no separate configuration needed. Setting sendUpdates="all" triggers Google's built-in invitation emails to all attendees.

Step 4: Build the Email Confirmation Tool#

For a more personalized confirmation beyond Google's default invite email, add a Gmail-based confirmation tool:

# tools/send_email.py
import base64
from email.mime.text import MIMEText
from googleapiclient.discovery import build
from langchain.tools import tool
from auth.google_auth import get_google_credentials

@tool
def send_confirmation_email(
    to_email: str,
    subject: str,
    body: str
) -> str:
    """
    Send a confirmation email via Gmail.

    Args:
        to_email: Recipient email address.
        subject: Email subject line.
        body: Plain-text email body.

    Returns:
        Confirmation that the email was sent.
    """
    creds = get_google_credentials()
    service = build("gmail", "v1", credentials=creds)

    message = MIMEText(body)
    message["to"] = to_email
    message["subject"] = subject

    raw = base64.urlsafe_b64encode(message.as_bytes()).decode()
    service.users().messages().send(
        userId="me",
        body={"raw": raw}
    ).execute()

    return f"Confirmation email sent to {to_email}."

Step 5: Assemble the Agent#

Now wire everything together using LangChain's create_openai_tools_agent:

# agent.py
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.memory import ConversationBufferMemory

from tools.availability import check_availability
from tools.create_event import create_event
from tools.send_email import send_confirmation_email

load_dotenv()

llm = ChatOpenAI(model="gpt-4o", temperature=0)

tools = [check_availability, create_event, send_confirmation_email]

system_prompt = """You are a professional meeting scheduling assistant. Your job is to:
1. Understand the meeting request (participants, duration, preferred time window).
2. Check availability for all attendees using the check_availability tool.
3. Identify the earliest mutually available slot that fits the requested duration.
4. Create the calendar event using the create_event tool.
5. Send a friendly confirmation email to the requester.

Always confirm the final meeting details before finishing. Use ISO 8601 format for datetimes.
Today's date and timezone will be provided in the user's message if relevant.
"""

prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

agent = create_openai_tools_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    memory=memory,
    verbose=True,
    max_iterations=10,
)

def schedule_meeting(user_request: str) -> str:
    result = agent_executor.invoke({"input": user_request})
    return result["output"]


if __name__ == "__main__":
    response = schedule_meeting(
        "Schedule a 30-minute product sync with alice@company.com and bob@company.com "
        "sometime on Thursday March 13, 2026 between 9am and 5pm Eastern. "
        "Title it 'Product Sync - Q1 Review'."
    )
    print(response)

Computer screen showing a calendar booking interface

Photo by Ed Hardie on Unsplash

Step 6: Testing the Agent#

Run a series of test cases to verify each tool fires correctly:

# test_agent.py
import pytest
from unittest.mock import patch, MagicMock
from tools.availability import check_availability
from tools.create_event import create_event

def test_check_availability_no_conflicts():
    """Should report attendee as free when no busy slots exist."""
    mock_result = {
        "calendars": {
            "alice@company.com": {"busy": []}
        }
    }
    with patch("tools.availability.get_calendar_service") as mock_svc:
        mock_svc.return_value.freebusy.return_value.query.return_value.execute.return_value = mock_result
        result = check_availability.invoke({
            "attendee_emails": ["alice@company.com"],
            "start_time": "2026-03-13T09:00:00-05:00",
            "end_time": "2026-03-13T17:00:00-05:00",
        })
    assert "Completely free" in result

def test_create_event_returns_link():
    """Should return event details including calendar link."""
    mock_event = {
        "htmlLink": "https://calendar.google.com/event?id=abc123",
        "conferenceData": {
            "entryPoints": [{"uri": "https://meet.google.com/xyz-abc-def"}]
        }
    }
    with patch("tools.create_event.get_google_credentials"), \
         patch("tools.create_event.build") as mock_build:
        mock_build.return_value.events.return_value.insert.return_value.execute.return_value = mock_event
        result = create_event.invoke({
            "title": "Test Meeting",
            "start_time": "2026-03-13T14:00:00-05:00",
            "end_time": "2026-03-13T14:30:00-05:00",
            "attendee_emails": ["alice@company.com"],
        })
    assert "Event created successfully" in result
    assert "calendar.google.com" in result

Run the tests:

pytest test_agent.py -v

For a full end-to-end test against real calendars, use test accounts in your Google Workspace domain to avoid polluting production calendars.

Production Considerations#

Rate limits: The Google Calendar API allows 1,000,000 queries per day for free. For high-volume deployments, implement exponential backoff with the tenacity library:

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=10))
def safe_freebusy_query(service, body):
    return service.freebusy().query(body=body).execute()

Timezone handling: Always store and pass explicit IANA timezone strings. Never rely on local system time. The agent prompt instructs the LLM to request timezone confirmation from the user if it is ambiguous.

Multi-calendar support: Enterprise users often have multiple calendars (personal, work, project-specific). Extend the check_availability tool to accept a calendar_id parameter and aggregate across multiple calendars using the calendarId field.

Security: Store your credentials.json and token.json outside your project root, and never commit them to version control. Use environment variables or a secrets manager in production.

Webhook-driven scheduling: For a fully automated pipeline, set up a Gmail push notification using Google Cloud Pub/Sub to trigger the agent when a scheduling-related email arrives. This eliminates the need for polling.

What to Build Next#

Now that you have a working meeting scheduler agent, consider extending it with:

  • Timezone negotiation — Automatically detect participant timezones from their Google profiles and suggest times that work across zones
  • Recurring meeting support — Handle requests like "schedule a weekly standup every Monday at 10am"
  • CRM integration — Pull attendee context from Salesforce or HubSpot before creating the event

Further Reading#

Frequently Asked Questions#

Do I need a paid Google Workspace account? No. The Google Calendar API is available on free personal Gmail accounts. However, for multi-user applications and enterprise features like delegated calendar access, a Google Workspace account is recommended.

Can this agent handle recurring meetings? Yes, with a small extension. Add a recurrence field to the event body in create_event:

"recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=MO"]

You would also need to update the agent's system prompt to teach it how to construct RRULE strings from natural language.

How do I handle conflicting time slots? The agent automatically handles this through its ReAct loop. When check_availability returns busy slots, the LLM identifies the next available window within the requested range and tries that instead. If no slot is found in the window, it asks the user for an alternative time range.

What LLMs work besides GPT-4o? Any LangChain-compatible model that supports tool calling works. Claude 3.5 Sonnet, Gemini 1.5 Pro, and Llama 3.1 (via Ollama) all support the tool-calling interface. Replace ChatOpenAI with the appropriate LangChain chat model class.