Build a Calendar Negotiation Agent That Schedules Meetings Over Email Using Groq and n8n
What We're Building
We're building a fully autonomous scheduling agent that lives inside n8n. It monitors a Gmail inbox, detects when someone wants to book a meeting, extracts their availability preferences using Groq's Mixtral model, checks your Google Calendar for open slots, and replies with a concrete, bookable time proposal.
Feature list:
- Inbound email monitoring for scheduling intent ("let's find time," "are you free next week?")
- LLM-powered extraction of preferred time windows, duration, and urgency
- Calendar availability lookup against your primary Google Calendar
- Automatic reply generation with 1–2 proposed slots
- Fully free-tier operational: Groq's generous free tokens, n8n's self-hosted community edition, and Google's free API quotas
This is not a chatbot. It's a silent infrastructure agent that reduces scheduling ping-pong to a single reply.
Architecture & Data Flow
Before we touch a node, let's map how the pieces communicate. The workflow is a linear pipeline with one decision gate.
Email arrives via Gmail trigger. We strip HTML and signatures to get clean text. That text hits Groq's Mixtral with a strict system prompt to output JSON. A Code node parses that JSON into n8n items. The Google Calendar node checks availability. If slots exist, we format them into a polite reply and send it. Every step logs to a simple database or spreadsheet for auditability.
Prerequisites (All Free Tier)
You need four accounts—all free to start:
-
n8n – Self-host via Docker or use n8n.cloud's free tier (limited executions). Install with:
docker run -it --rm --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n n8nio/n8nAccess at
http://localhost:5678. -
Groq API Key – Sign up at console.groq.com. Free tier gives you millions of tokens per day on Mixtral-8x7b. Generate an API key under "API Keys."
-
Google Cloud Project – Go to console.cloud.google.com. Create a project, enable the Gmail API and Google Calendar API. Create OAuth 2.0 credentials (Desktop app type). Download the JSON. n8n uses this for the Gmail and Calendar nodes.
-
Gmail Account – The inbox you want to monitor. We'll authenticate n8n to read, search, and send on your behalf.
Step 1: Configure Gmail Trigger Node
In your n8n editor, add a Gmail Trigger node. Set:
- Authentication: Connect your OAuth2 credentials (upload the JSON from Google Cloud).
- Events:
Message Received - Polling Interval:
1 minute(or5 minutesif you want to stay well under quota on free tier) - Format:
Resolved(gives us full headers, body, and attachments) - Simplify:
On(removes nested JSON clutter)
Test the node by sending yourself an email with scheduling language. You should see the email data populate in the output panel.
Step 2: Extract Email Content & Filter Noise
Gmail delivers HTML bodies. We need clean text for the LLM. Add a Function node (or Code node in JavaScript mode) after the trigger.
// Extract plain text from email body, remove signatures and quoted replies
const htmlBody = $input.item.json.body?.html || $input.item.json.body?.plain || '';
// Strip HTML tags
let plainText = htmlBody.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
// Remove common signature patterns and quoted replies
plainText = plainText
.split(/On .* wrote:|--\s*$|Best,|Cheers,|Thanks,/i)[0]
.trim();
// Remove any remaining email threads (lines starting with >)
plainText = plainText.split('\n').filter(line => !line.startsWith('>')).join('\n');
return {
from: $input.item.json.from?.value?.[0]?.address || $input.item.json.from,
subject: $input.item.json.subject,
body: plainText,
threadId: $input.item.json.threadId
};
This gives us a clean body field. We also pass through from, subject, and threadId for context and reply threading.
Step 3: Build the Groq Intent Extraction Node
Add an HTTP Request node. This is where the agent's brain lives.
- Method: POST
- URL:
https://api.groq.com/openai/v1/chat/completions - Authentication: Header Auth
- Name:
Authorization - Value:
Bearer YOUR_GROQ_API_KEY
- Name:
- Headers: Add
Content-Type: application/json - Body (JSON):
{
"model": "mixtral-8x7b-32768",
"temperature": 0.1,
"response_format": { "type": "json_object" },
"messages": [
{
"role": "system",
"content": "You are a scheduling extraction engine. Analyze the email and output JSON. If the email contains a meeting request or scheduling intent, extract: preferred_date_ranges (array of date strings or 'any'), preferred_times (array of time windows like 'morning', 'afternoon', '2pm-4pm'), duration_minutes (integer), urgency (low/medium/high), and topic (short string). If no scheduling intent is detected, set intent_detected to false. Never invent dates. Use only what's explicitly in the email."
},
{
"role": "user",
"content": "Subject: {{ $json.subject }}\nFrom: {{ $json.from }}\nBody: {{ $json.body }}"
}
]
}
Groq's Mixtral is blazing fast (often <1 second) and free-tier friendly. We set temperature low for deterministic extraction. The response_format forces structured JSON output.
Step 4: Parse Structured Data with a Code Node
The HTTP Request node returns the API response. We need to extract the actual JSON from the LLM and split it into n8n items if multiple date ranges exist.
Add a Code node:
const response = $input.item.json.choices[0].message.content;
let parsed;
try {
parsed = JSON.parse(response);
} catch (e) {
// If parsing fails, treat as no intent
parsed = { intent_detected: false };
}
// If no scheduling intent, stop the workflow
if (!parsed.intent_detected && parsed.intent_detected !== undefined) {
throw new Error('No scheduling intent detected');
}
// Create an item for each preferred date range
const items = (parsed.preferred_date_ranges || ['any']).map(dateRange => ({
json: {
from: $input.item.json.from,
subject: $input.item.json.subject,
threadId: $input.item.json.threadId,
dateRange: dateRange,
preferredTimes: parsed.preferred_times || ['morning'],
duration: parsed.duration_minutes || 30,
urgency: parsed.urgency || 'medium',
topic: parsed.topic || 'Meeting'
}
}));
return items;
This node handles malformed LLM responses gracefully and splits the workflow if the sender gave multiple date options like "Monday or Wednesday."
Step 5: Check Calendar Availability
Add a Google Calendar node with the Event: Get All operation.
- Calendar: Your primary calendar (usually your email address)
- Time Range: We need to dynamically set this based on the extracted date range. Use expressions:
- Start:
{{ new Date($json.dateRange).toISOString() }}(or use a calculated start if "any") - End:
{{ new Date(new Date($json.dateRange).getTime() + 24*60*60*1000).toISOString() }}
- Start:
For "any" or relative dates ("next week"), add a small Code node before this to resolve them:
const dateRange = $input.item.json.dateRange;
let start, end;
const now = new Date();
if (dateRange === 'any' || dateRange === 'next week') {
start = new Date(now.getFullYear(), now.getMonth(), now.getDate() + (7 - now.getDay() + 1));
end = new Date(start.getTime() + 7 * 24 * 60 * 60 * 1000);
} else {
start = new Date(dateRange);
end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
}
return {
...$input.item.json,
searchStart: start.toISOString(),
searchEnd: end.toISOString()
};
Feed these into the Calendar node's time range fields.
The Calendar node returns existing events. We now need to find open slots that match the preferred times and duration.
Step 6: Propose Times and Send the Reply
Add a final Code node that computes free slots and formats the email reply.
const events = $input.all().filter(item => item.json.kind === 'calendar#event');
const { duration, preferredTimes, from, subject, threadId, topic } = $input.first().json;
// Define business hours based on preferred time windows
const hourMap = {
morning: [8, 12],
afternoon: [12, 17],
evening: [17, 20],
'2pm-4pm': [14, 16]
};
let searchWindows = [];
for (const time of preferredTimes) {
const range = hourMap[time] || hourMap.morning;
searchWindows.push(range);
}
// Find free slots
const freeSlots = [];
const searchStart = new Date($input.first().json.searchStart);
const searchEnd = new Date($input.first().json.searchEnd);
for (let d = new Date(searchStart); d < searchEnd; d.setDate(d.getDate() + 1)) {
for (const [startH, endH] of searchWindows) {
let slotStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), startH, 0, 0);
const slotEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), endH, 0, 0);
while (slotStart.getTime() + duration * 60000 <= slotEnd.getTime()) {
const slotEndCandidate = new Date(slotStart.getTime() + duration * 60000);
const conflicts = events.some(event => {
const eventStart = new Date(event.json.start?.dateTime || event.json.start?.date);
const eventEnd = new Date(event.json.end?.dateTime || event.json.end?.date);
return slotStart < eventEnd && slotEndCandidate > eventStart;
});
if (!conflicts) {
freeSlots.push({
start: new Date(slotStart),
end: slotEndCandidate
});
if (freeSlots.length >= 2) break;
}
slotStart = new Date(slotStart.getTime() + 30 * 60000);
}
if (freeSlots.length >= 2) break;
}
if (freeSlots.length >= 2) break;
}
if (freeSlots.length === 0) {
return {
replyBody: `Thanks for reaching out! Unfortunately, I don't see availability matching your request for "${topic}". Could you suggest alternative times?`,
to: from,
subject: `Re: ${subject}`,
threadId
};
}
const options = freeSlots.map(s =>
`${s.start.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })} at ${s.start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`
).join(' or ');
return {
replyBody: `Hi! I'd be happy to meet about "${topic}". How about ${options}? Let me know if either works, and I'll send a calendar invite.`,
to: from,
subject: `Re: ${subject}`,
threadId
};
Finally, add a Gmail Send node:
- To:
{{ $json.to }} - Subject:
{{ $json.subject }} - Body:
{{ $json.replyBody }} - Thread ID:
{{ $json.threadId }}(this ensures replies stay in the same thread)
For an even smarter system, check out our guide on building a Gmail AI triage agent that drafts replies using Gemini and Groq.
Step 7: Testing and Running the Workflow
- Send a test email to your monitored inbox: "Hey, can we chat about the Q4 roadmap next Tuesday or Thursday afternoon? 30 minutes should do it."
- Watch the n8n execution log. You should see the Gmail trigger fire, the Groq node extract dates, and the Calendar node check availability.
- Within ~30 seconds, you'll receive a reply proposing specific times.
To activate for continuous operation, toggle the workflow to Active in n8n. The trigger will poll every minute.
Extensions That Make It Production-Ready
1. Add a Human-in-the-Loop Approval Gate Before sending, route the proposed reply to a Slack channel using n8n's Slack node. Add a manual approval step. This prevents the agent from sending replies you haven't vetted—critical for early iterations.
2. Persist Negotiation State Right now, each email is stateless. If someone replies "Tuesday doesn't work, how about Wednesday?", the agent treats it as a fresh scheduling request. To handle multi-turn negotiation, store the thread state in Supabase or SQLite. For a deeper dive on persistent AI assistants, see our Notion knowledge assistant build that answers questions from your workspace.
3. Auto-Create Calendar Events Once the human approves a slot (or if you're bold enough to go fully autonomous), use the Google Calendar Create Event node to book the meeting and include a Google Meet link.
4. Handle Time Zones
Add timezone detection from the sender's email domain or signature. Use Intl.DateTimeFormat in a Code node to convert proposed times to the sender's local zone.
5. Personalization from CRM If you're using this for client meetings, pull context from a CSV or database. Our cold outreach email personalizer from a CSV of prospects shows the pattern for injecting prospect data into LLM prompts.
Common Pitfalls and How to Avoid Them
Groq Rate Limits The free tier is generous but not infinite. If you process hundreds of emails per hour, add a Wait node between the trigger and the Groq call (5–10 seconds) to space out API calls. Monitor your usage at console.groq.com.
LLM Hallucinated Dates
Mixtral occasionally invents dates when the email is vague. The temperature: 0.1 setting helps, but always validate. Add a Code node that checks if extracted dates are within a reasonable future window (e.g., next 30 days). If not, default to "next week" and let the Calendar node search broadly.
Google API Quotas The Gmail API free tier allows 1 billion quota units per day, but polling every minute on a busy inbox can edge into higher usage. Set the trigger to 5-minute intervals if you're not expecting real-time scheduling needs.
OAuth Token Expiry
Google OAuth tokens expire. n8n handles refresh tokens if you request offline access during the OAuth flow. Make sure your Google Cloud Console app is in "Testing" mode (not "Production") to avoid verification requirements while building.
Email Threading Breaks
If the threadId isn't passed correctly, replies start new threads. Always map the threadId from the trigger output through every node to the Gmail Send node.
FAQ
Q: Why Groq instead of OpenAI or Anthropic? Speed and free tier. Groq serves Mixtral at 400+ tokens per second with a generous daily free limit. For structured extraction tasks, it's hard to beat on latency or cost.
Q: Can I use this for multiple calendars? Yes. In the Google Calendar node, switch to "All Calendars" or specify a list. The availability check will scan across them.
Q: What if the sender proposes multiple durations ("30 min or 1 hour")?
Extend the Groq prompt to output an array for duration_minutes. In the availability Code node, iterate through durations and propose the longest slot that fits.
Q: How do I handle spam or non-scheduling emails?
The Groq node will return intent_detected: false for most non-scheduling emails, and the Code node in Step 4 throws an error, stopping execution. Add a Filter node before the Groq call to skip emails with common spam patterns.
Q: Is this secure for production use? All API keys are stored in n8n's encrypted credentials store. Email content passes through Groq's API—review their data usage policy. For regulated environments, consider running Mixtral locally via Ollama and replacing the HTTP Request node with a local endpoint. If you're deploying LLM features in enterprise settings, our case study on deploying an LLM feature at a regulated enterprise in 4 weeks covers the compliance playbook.
Q: How do I make this agent handle multi-turn negotiation? Store the conversation state (proposed times, accepted/rejected) in a database keyed by threadId. On subsequent emails in the same thread, load the state and include it in the Groq prompt. This transforms the agent from a single-shot proposer to a true negotiator.
Want to build like a Forward Deployed Engineer?
FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.
Explore the program