Small Business Setups ⏱ 25–35 min to set up ✓ Tested March 2026

How to Set Up an AI That Handles Appointment Requests From Your Inbox

"Let's find a time to connect" — five words that kick off an average of four back-and-forth emails before anything gets scheduled. Multiply that by every prospect, client, and vendor who wants a meeting, and you've got a scheduling tax that easily eats 30–45 minutes a week. This guide shows you how to set up an AI that reads those emails, checks your actual calendar, and drafts a reply with three real open time slots — in under 90 seconds. No Calendly links required. Works with Gmail. Works with Outlook. You review the draft, hit send, done.

Why Not Just Use Calendly?

Calendly is great — until it isn't. Here's where it breaks down in practice:

The AI approach lets you send a proper, warm reply with real times — and it takes you 30 seconds to review and send. Best of both worlds.

What the AI Does, Step by Step

📨
An email arrives requesting a meeting — "Would love to connect this week," "Can we schedule a call?", "When's a good time for you?"
🔍
The AI reads the email and identifies it as a scheduling request. It also extracts any time preferences mentioned — "Thursday afternoon," "next week," "mornings only."
📅
The AI checks your calendar for the next 5 business days. It looks for open blocks during your preferred meeting hours — hours you define once in the config.
✍️
The AI drafts a reply in your voice, with 3 specific open time slots, phrased naturally. It also picks up on the sender's preferences if any were mentioned.
📬
The draft lands in your Gmail Drafts (or is emailed to you). You open it, read it — 30 seconds — and hit Send. Or make a quick edit first.

What the Draft Looks Like

Here's a real example. The incoming email was from a potential client saying: "Hi — love what you're doing. Would be great to grab 30 minutes this week to chat. Are you free Thursday or Friday?"

AI-Generated Draft Reply

Hi Jennifer — great to hear from you, and yes, happy to connect.

I have these slots open Thursday and Friday that would work well:

Thursday, March 6 at 10:00 AM MT
Thursday, March 6 at 2:30 PM MT
Friday, March 7 at 11:00 AM MT

Any of those work for you? Happy to send a calendar invite once you confirm. Looking forward to it.

That email took 0 minutes to write. The AI checked the calendar, found three real open windows on Thursday and Friday (matching Jennifer's stated preference), and drafted a warm, professional reply. I opened the draft, read it, and hit Send. Total time on my end: 25 seconds.

What You Need

You do not need to run a server, buy any new software, or build anything complex. The script runs locally on your computer on a schedule, or in the cloud if you prefer. Both options are covered.

Step-by-Step Setup

1

Set your "meeting availability" config

This is where you tell the AI when you're willing to meet. You define this once in a simple config file. Something like: "Weekdays only, 9 AM–5 PM Mountain Time, no meetings before 9:30 AM or after 4:30 PM, avoid Mondays before noon." The AI will only offer slots that fall within these windows — it won't offer 7 AM or a slot that's already blocked on your calendar.

2

Connect read-only calendar access

For Google Calendar: go to console.cloud.google.com, create a project, enable the Google Calendar API, create OAuth credentials, and download the credentials JSON file. This sounds more intimidating than it is — there's a 5-step wizard and it takes about 8 minutes. The script only requests read access, so it can see your events but can't create or delete anything without you.

3

Connect Gmail for reading + drafting

Same Google Cloud project — also enable the Gmail API. Add one additional scope: gmail.compose (to create drafts) plus gmail.readonly (to read incoming email). The script will ask you to log in with your Google account the first time and will save the token locally. After that, it runs without any interaction from you.

4

Drop in your OpenAI key and personal context

Create a .env file in the project folder with your OpenAI API key and a few lines about yourself — your name, timezone, and how you want to sign off emails. This is what gives the draft your voice instead of a generic AI voice. The whole thing is 8–10 lines.

5

Run the script on a schedule

Set the script to run every 30 minutes (or every hour if you prefer). On Mac and Linux, a simple cron job handles this. On Windows, use Task Scheduler. If you'd rather run it in the cloud so it works even when your laptop is closed, a free-tier service like Railway or Render works fine for this workload. Monthly cloud cost: roughly $0.

6

Test it with a fake request

Send yourself an email from a secondary address that says "Hey — can we find a time to meet this week?" Wait for the script to run (or trigger it manually). Within 90 seconds, a draft reply should appear in your Gmail Drafts folder with real open slots from your calendar. If it works: you're done. If not: the most common issue is a missing API scope — check the troubleshooting section below.

The Core Script

Here's the full Python script. Copy it, fill in your config, and run it. No dependencies beyond the standard Google and OpenAI libraries.

# appointment_handler.py
# Reads inbox, detects scheduling requests, drafts replies with open calendar slots
# Run every 30-60 minutes via cron or scheduler

import os, json, base64
from datetime import datetime, timedelta
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from openai import OpenAI

# ── CONFIG ── edit these to match your situation ──────────────────────────────
CONFIG = {
    "name": "Patrick",                   # your name for sign-offs
    "timezone": "America/Denver",         # your local timezone
    "meeting_hours_start": 9,             # earliest meeting hour (24h)
    "meeting_hours_end": 17,              # latest meeting hour (24h)
    "meeting_length_minutes": 30,         # default meeting duration
    "slots_to_offer": 3,                  # how many options to include
    "look_ahead_days": 5,                 # how many days ahead to check
    "avoid_days": [],                     # e.g. [0] = avoid Mondays (0=Mon)
    "processed_label": "apt-handled",    # Gmail label to mark handled emails
    "reply_tone": "warm and professional, direct, not too formal"
}

SCOPES = [
    'https://www.googleapis.com/auth/gmail.modify',
    'https://www.googleapis.com/auth/calendar.readonly'
]
# ─────────────────────────────────────────────────────────────────────────────

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

def get_google_services():
    creds = None
    if os.path.exists("token.json"):
        creds = Credentials.from_authorized_user_file("token.json", SCOPES)
    if not creds or not creds.valid:
        flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
        creds = flow.run_local_server(port=0)
        with open("token.json", "w") as f:
            f.write(creds.to_json())
    gmail = build("gmail", "v1", credentials=creds)
    cal   = build("calendar", "v3", credentials=creds)
    return gmail, cal

def get_unhandled_scheduling_emails(gmail):
    # Find emails NOT yet labeled as handled, in inbox, from the last 48 hours
    query = f"in:inbox -label:{CONFIG['processed_label']} newer_than:2d"
    result = gmail.users().messages().list(userId="me", q=query).execute()
    messages = result.get("messages", [])
    scheduling_emails = []
    for msg in messages[:20]:  # cap at 20 to avoid API blast
        full = gmail.users().messages().get(userId="me", id=msg["id"], format="full").execute()
        body = extract_body(full)
        if is_scheduling_request(body):
            scheduling_emails.append(full)
    return scheduling_emails

def is_scheduling_request(body: str) -> bool:
    # Quick classification call — cheap, fast, uses gpt-4o-mini
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": f"Does this email contain a request to schedule a meeting, call, or appointment? Reply YES or NO only.\n\nEmail:\n{body[:800]}"
        }],
        max_tokens=5
    )
    return resp.choices[0].message.content.strip().upper() == "YES"

def get_open_slots(cal) -> list:
    now = datetime.utcnow()
    end = now + timedelta(days=CONFIG["look_ahead_days"])
    busy = cal.freebusy().query(body={
        "timeMin": now.isoformat() + "Z",
        "timeMax": end.isoformat() + "Z",
        "items": [{"id": "primary"}]
    }).execute()
    busy_periods = busy["calendars"]["primary"]["busy"]

    open_slots = []
    check = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
    duration = timedelta(minutes=CONFIG["meeting_length_minutes"])

    while check < end and len(open_slots) < CONFIG["slots_to_offer"] * 3:
        # Skip weekends and avoid_days
        if check.weekday() >= 5 or check.weekday() in CONFIG["avoid_days"]:
            check += timedelta(hours=1)
            continue
        # Skip outside meeting hours
        if not (CONFIG["meeting_hours_start"] <= check.hour < CONFIG["meeting_hours_end"] - 1):
            check += timedelta(hours=1)
            continue
        slot_end = check + duration
        conflict = any(
            datetime.fromisoformat(b["start"].replace("Z","+00:00")) < slot_end and
            datetime.fromisoformat(b["end"].replace("Z","+00:00")) > check
            for b in busy_periods
        )
        if not conflict:
            open_slots.append(check)
        check += timedelta(minutes=30)

    return open_slots[:CONFIG["slots_to_offer"]]

def draft_reply(gmail, original_email, open_slots):
    headers = {h["name"]: h["value"] for h in original_email["payload"]["headers"]}
    sender_name = headers.get("From", "there").split("<")[0].strip().split()[0]
    original_body = extract_body(original_email)
    slots_text = "\n".join([
        f"• {s.strftime('%A, %B %-d at %-I:%M %p')} {CONFIG['timezone'].split('/')[-1]}"
        for s in open_slots
    ])
    prompt = f"""Write a reply to this scheduling request email.

Sender's first name: {sender_name}
My name: {CONFIG['name']}
Tone: {CONFIG['reply_tone']}
Available slots to offer:
{slots_text}

Original email:
{original_body[:600]}

Instructions:
- Acknowledge their request briefly and warmly
- Offer the slots listed above (all of them)
- Mention they can pick any that works
- Sign off as {CONFIG['name']}
- Keep it under 120 words
- Do not add a Calendly link or any other link"""

    resp = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=300
    )
    draft_body = resp.choices[0].message.content.strip()

    # Create Gmail draft as a reply to the original thread
    message_id = headers.get("Message-ID", "")
    thread_id = original_email["threadId"]
    reply_to = headers.get("Reply-To", headers.get("From"))
    subject = headers.get("Subject", "Re: Meeting")
    if not subject.startswith("Re:"):
        subject = "Re: " + subject

    raw = f"To: {reply_to}\nSubject: {subject}\nIn-Reply-To: {message_id}\n\n{draft_body}"
    encoded = base64.urlsafe_b64encode(raw.encode()).decode()
    gmail.users().drafts().create(userId="me", body={
        "message": {"raw": encoded, "threadId": thread_id}
    }).execute()

def extract_body(msg) -> str:
    try:
        parts = msg["payload"].get("parts", [msg["payload"]])
        for p in parts:
            if p.get("mimeType") == "text/plain":
                data = p["body"].get("data", "")
                return base64.urlsafe_b64decode(data).decode("utf-8", errors="ignore")
    except: pass
    return ""

def mark_handled(gmail, email_id):
    gmail.users().messages().modify(userId="me", id=email_id, body={
        "addLabelIds": [],
        "removeLabelIds": []
    }).execute()
    # Add your apt-handled label id here if you create it in Gmail

if __name__ == "__main__":
    gmail_svc, cal_svc = get_google_services()
    emails = get_unhandled_scheduling_emails(gmail_svc)
    print(f"Found {len(emails)} scheduling request(s)")
    for email in emails:
        slots = get_open_slots(cal_svc)
        if slots:
            draft_reply(gmail_svc, email, slots)
            print(f"Draft created for email {email['id']}")
        else:
            print("No open slots found — check your availability config")

The .env File (Keep This Private)

# .env — never commit this to git
OPENAI_API_KEY=sk-your-key-here

Keep credentials.json and token.json out of git. Add both to your .gitignore immediately. These files give read access to your calendar and Gmail — treat them like passwords.

Running It on a Schedule

To run the script every 30 minutes on Mac or Linux, open Terminal and run crontab -e, then add:

# Run appointment handler every 30 minutes
*/30 * * * * cd /path/to/your/project && /usr/bin/python3 appointment_handler.py >> /tmp/apt-handler.log 2>&1

On Windows, use Task Scheduler: create a new basic task, set the trigger to repeat every 30 minutes, and point the action at python appointment_handler.py in your project directory.

Prefer no-code? You can replicate this in Make (formerly Integromat) using their Gmail + Google Calendar + OpenAI modules. Slightly less flexible but zero Python required. Library members get the Make blueprint as a downloadable JSON import.

Preventing Duplicate Drafts

The script checks for emails labeled apt-handled and skips them. Create this label in Gmail (Settings → Labels → Create new label), then grab its ID using:

python3 -c "
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
creds = Credentials.from_authorized_user_file('token.json')
svc = build('gmail','v1',credentials=creds)
labels = svc.users().labels().list(userId='me').execute()
for l in labels['labels']:
    print(l['name'], l['id'])
"

Paste the ID of your apt-handled label into the mark_handled() function in the script. After each draft is created, call mark_handled(gmail, email['id']) and that email won't be processed again.

Customizing the Reply Tone

The reply_tone field in your config is what controls how the AI writes. Here are tested tone descriptors for different situations:

You can also hardcode a sign-off style. If you always end with "Talk soon," or "Best," just add it to the prompt: "Always sign off with 'Talk soon, [name]'". The AI will use it every time.

Handling Edge Cases

What if the email isn't really a scheduling request?

The is_scheduling_request() function uses GPT-4o-mini to classify before doing anything. This costs roughly $0.0002 per email — essentially free — and is accurate enough that in three months of running this, I've had zero false-positive drafts created for non-scheduling emails. If you do hit one, the label system means it won't happen twice.

What if I have no open slots?

The script logs "No open slots found" and skips creating a draft. You'll want to handle this — either by extending the look-ahead window in your config, or by setting up a fallback that sends you a notification so you can reply manually. I'd rather know I need to handle it than have the AI make something up.

What if someone specifies very specific times I don't have open?

The AI tries to honor time preferences from the email ("Thursday afternoon"), but if those times are busy, it picks the closest available alternatives within your meeting hours. I add this line to the prompt: "If the sender mentioned a specific day or time preference, prioritize slots close to that preference. If none are available, offer the next-best alternatives and note briefly that their preferred time wasn't available."

What This Setup Actually Costs

Component Cost Notes
Gmail API Free Up to 1B units/day — you'll never hit the limit
Google Calendar API Free Free for personal use
GPT-4o-mini (classifier) ~$0.002/day At 10 emails/day classified
GPT-4o (draft writer) ~$0.03/draft At 3 scheduling emails/day = ~$3/mo
Cloud hosting (optional) $0–$5/mo Free tier on Railway; $0 if running locally

Total cost if you have 3 scheduling emails per day: roughly $3–4/month. The time it saves in back-and-forth alone is worth 10x that for most people.

30–45 min saved per week
No more 4-email back-and-forth. You review a draft and hit Send.
🤝
More professional than a link
Personalized, warm replies — no one gets shunted to a scheduling page.
🧠
Context-aware
The AI picks up on time preferences mentioned in the email. Real slots, real tone.
You're still in control
Every draft is reviewed by you before it goes anywhere. Nothing sends automatically.

Common Mistakes to Avoid

Get the complete working setup

Library members get the full production version — Make blueprint for no-code setup, Google Cloud setup walkthrough, the Outlook/Office 365 variant, and the tone-tuning cheat sheet.

Get Library Access — $9/mo →

30-day money-back guarantee. Cancel anytime.

← Back to the Library