Build a Gmail AI Triage Agent That Drafts Replies Using Gemini and Groq
What We're Building
A Gmail add-on that operates as your personal triage assistant. For every new email that hits your inbox, the agent:
- Reads the sender, subject, and body.
- Classifies priority into three buckets:
URGENT,NEEDS_REPLY, orLOW_PRIORITY. - Applies a Gmail label matching the priority so you can visually scan your inbox.
- Drafts a context-aware reply saved directly in the thread, ready for one-click review and send.
The entire pipeline runs on free-tier APIs: Google Apps Script for execution, Groq (Llama 3.1 70B) for fast, cheap classification, and Gemini 1.5 Flash for high-quality reply drafting. No servers, no Docker, no credit card.
Feature List
| Feature | Implementation |
|---|---|
| Inbox polling | Apps Script time-driven trigger (every 5 minutes) |
| Email extraction | GmailApp service, parses plain-text body |
| Priority classification | Groq Chat Completions API (Llama 3.1 70B) |
| Label application | GmailApp.createLabel() + addLabelToThread() |
| Reply drafting | Gemini 1.5 Flash via UrlFetchApp |
| Draft storage | GmailApp.createDraft() on the original thread |
| Idempotency guard | Custom PROCESSED label to skip already-handled threads |
Architecture Overview
The flow is linear but contains a conditional branch: only emails classified as URGENT or NEEDS_REPLY trigger reply drafting. LOW_PRIORITY emails get labeled and skipped. Every processed thread receives a PROCESSED label so the next poll cycle ignores it—this is your idempotency mechanism without a database.
Prerequisites (Free Tier)
You need three things, all free:
- Google Account with Gmail enabled. You'll use the same account to create the Apps Script project.
- Groq API Key — sign up at console.groq.com. Free tier gives you generous requests per minute on Llama 3.1 70B.
- Gemini API Key — grab it from aistudio.google.com. Free tier includes 15 requests per minute on Gemini 1.5 Flash, more than enough for personal inbox triage.
No other dependencies. Apps Script's UrlFetchApp handles all HTTP calls natively—no npm, no bundler.
Step 1: Scaffold the Google Apps Script Project
Navigate to script.google.com and create a new project. Delete the boilerplate myFunction and replace it with our configuration skeleton:
// Configuration
const CONFIG = {
GROQ_API_KEY: 'YOUR_GROQ_API_KEY',
GEMINI_API_KEY: 'YOUR_GEMINI_API_KEY',
PROCESSED_LABEL: 'PROCESSED',
PRIORITY_LABELS: {
URGENT: 'URGENT',
NEEDS_REPLY: 'NEEDS_REPLY',
LOW_PRIORITY: 'LOW_PRIORITY'
},
MAX_EMAILS_PER_RUN: 10
};
// Ensure all required labels exist
function ensureLabels() {
const labels = [
CONFIG.PROCESSED_LABEL,
...Object.values(CONFIG.PRIORITY_LABELS)
];
labels.forEach(labelName => {
const existing = GmailApp.getUserLabelByName(labelName);
if (!existing) {
GmailApp.createLabel(labelName);
}
});
}
ensureLabels() creates the four labels we need: PROCESSED, URGENT, NEEDS_REPLY, and LOW_PRIORITY. Call it once manually from the Apps Script editor to bootstrap.
Step 2: Implement the Priority Classifier with Groq
Groq's API is OpenAI-compatible, so the request shape is familiar. We'll send the email metadata and ask for a structured JSON response with a priority field.
function classifyPriority(emailData) {
const prompt = `You are an email triage classifier. Given the following email, respond with ONLY a JSON object containing a single key "priority" with one of these values: "URGENT", "NEEDS_REPLY", or "LOW_PRIORITY".
Rules:
- URGENT: Time-sensitive, requires immediate action within hours, or from a key stakeholder.
- NEEDS_REPLY: Requires a response but not immediately critical.
- LOW_PRIORITY: Newsletters, notifications, CC-only threads, or no action required.
Email:
From: ${emailData.from}
Subject: ${emailData.subject}
Body: ${emailData.body.substring(0, 2000)}
JSON:`;
const options = {
method: 'post',
headers: {
'Authorization': `Bearer ${CONFIG.GROQ_API_KEY}`,
'Content-Type': 'application/json'
},
payload: JSON.stringify({
model: 'llama-3.1-70b-versatile',
messages: [
{ role: 'system', content: 'You are a precise email classifier. Output valid JSON only.' },
{ role: 'user', content: prompt }
],
temperature: 0.1,
max_tokens: 50
}),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch('https://api.groq.com/openai/v1/chat/completions', options);
const json = JSON.parse(response.getContentText());
const content = json.choices[0].message.content.trim();
// Parse the JSON from the response, stripping any markdown fences
const cleanContent = content.replace(/```json|```/g, '').trim();
return JSON.parse(cleanContent).priority;
}
Key decisions here:
- Temperature 0.1 keeps classification deterministic—you don't want the same email flipping between
URGENTandLOW_PRIORITYon different runs. - max_tokens 50 is tight because we only need
{"priority":"URGENT"}. This keeps latency under 500ms on Groq's hardware. - We truncate the body to 2000 characters. Llama 3.1 70B has a massive context window, but for classification, the first 2000 chars of an email body contain all the signal you need.
Step 3: Draft Context-Aware Replies with Gemini
Gemini 1.5 Flash is ideal here: it's fast, free-tier generous, and handles instruction-following well for structured email replies. We'll use the REST API via UrlFetchApp.
function draftReply(emailData) {
const prompt = `You are an executive assistant drafting email replies. Write a concise, professional reply to the email below. The reply should:
- Acknowledge receipt.
- Answer any questions if possible, or indicate when you'll follow up.
- Be 2-4 sentences unless the email demands more detail.
- Sound like a real human wrote it—no corporate jargon.
- Start with "Hi [Name]," using the sender's first name if available.
Original Email:
From: ${emailData.from}
Subject: ${emailData.subject}
Body: ${emailData.body.substring(0, 3000)}
Draft reply:`;
const options = {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
payload: JSON.stringify({
contents: [{
parts: [{ text: prompt }]
}],
generationConfig: {
temperature: 0.7,
maxOutputTokens: 500
}
}),
muteHttpExceptions: true
};
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${CONFIG.GEMINI_API_KEY}`;
const response = UrlFetchApp.fetch(url, options);
const json = JSON.parse(response.getContentText());
return json.candidates[0].content.parts[0].text;
}
Note the Gemini API uses a different request shape than OpenAI-compatible APIs. The contents[].parts[] structure is Gemini-specific. Temperature 0.7 gives natural variation without hallucination.
Step 4: Assemble the Main Triage Pipeline
Now we wire everything together in a single processInbox() function:
function processInbox() {
ensureLabels();
const processedLabel = GmailApp.getUserLabelByName(CONFIG.PROCESSED_LABEL);
const urgentLabel = GmailApp.getUserLabelByName(CONFIG.PRIORITY_LABELS.URGENT);
const needsReplyLabel = GmailApp.getUserLabelByName(CONFIG.PRIORITY_LABELS.NEEDS_REPLY);
const lowPriorityLabel = GmailApp.getUserLabelByName(CONFIG.PRIORITY_LABELS.LOW_PRIORITY);
// Fetch unprocessed threads from inbox
const query = `in:inbox -label:${CONFIG.PROCESSED_LABEL}`;
const threads = GmailApp.search(query, 0, CONFIG.MAX_EMAILS_PER_RUN);
threads.forEach(thread => {
const messages = thread.getMessages();
const latestMessage = messages[messages.length - 1];
const emailData = {
from: latestMessage.getFrom(),
subject: thread.getFirstMessageSubject(),
body: latestMessage.getPlainBody()
};
try {
// Step 1: Classify
const priority = classifyPriority(emailData);
// Step 2: Apply priority label
let priorityLabel;
switch (priority) {
case 'URGENT':
priorityLabel = urgentLabel;
break;
case 'NEEDS_REPLY':
priorityLabel = needsReplyLabel;
break;
default:
priorityLabel = lowPriorityLabel;
}
thread.addLabel(priorityLabel);
// Step 3: Draft reply if needed
if (priority === 'URGENT' || priority === 'NEEDS_REPLY') {
const replyDraft = draftReply(emailData);
GmailApp.createDraft(
thread.getId(),
replyDraft,
{
subject: `Re: ${emailData.subject}`,
htmlBody: replyDraft.replace(/\n/g, '<br>')
}
);
}
// Step 4: Mark as processed
thread.addLabel(processedLabel);
} catch (error) {
console.error(`Failed processing thread ${thread.getId()}: ${error}`);
// Still mark as processed to avoid infinite retry loops
thread.addLabel(processedLabel);
}
});
}
Critical design choice: we mark the thread as processed even on failure. Without this, a persistent error (e.g., malformed email body causing JSON parse failure) would block the entire queue every cycle. Log the error, move on.
Step 5: Deploy as a Time-Driven Trigger
In the Apps Script editor:
- Click the clock icon (Triggers) in the left sidebar.
- Click + Add Trigger.
- Configure:
- Function:
processInbox - Deployment:
Head - Event source:
Time-driven - Type:
Minutes timer - Interval:
Every 5 minutes
- Function:
- Click Save.
Google will prompt you to authorize the script. Grant the requested Gmail permissions—these stay scoped to your account only.
The first run will process up to 10 unprocessed emails. Subsequent runs handle new arrivals. At 5-minute intervals, you'll process 120 emails/hour max, which is well within free-tier rate limits for both Groq and Gemini.
Running the Agent
Once deployed, the agent runs silently in the background. Here's what to expect:
- Inbox labels appear automatically. Open Gmail and you'll see the
URGENT,NEEDS_REPLY, andLOW_PRIORITYlabels populate on new threads. - Drafts materialize in threads. Open any
URGENTorNEEDS_REPLYthread and you'll find a draft reply waiting. Review, edit if needed, and hit send. - Execution logs live at Executions in the Apps Script dashboard. Check here if something looks off.
To test manually before relying on the trigger, run processInbox() once from the Apps Script editor and watch the logs.
Sensible Extensions
Once the core loop works, you can layer on sophistication without leaving the free tier:
- Sender-specific rules. Maintain a
Configsheet (viaSpreadsheetApp) mapping specific senders to always-URGENT or always-LOW_PRIORITY overrides. This reduces API calls for known patterns. - Reply style customization. Pass a
toneparameter to Gemini—"formal","casual","brief"—based on the sender's domain or your relationship. - Attachment-aware drafting. Check
latestMessage.getAttachments()and include attachment filenames in the Gemini prompt so the draft acknowledges received files. - Multi-lingual replies. Detect the email's language (Gemini can do this in the same call) and draft in the same language.
- Slack notification for URGENT. Use the free Slack Incoming Webhook to ping a channel when an
URGENTemail arrives. One extraUrlFetchAppcall in theURGENTbranch.
If you enjoy stitching LLMs into workflows like this, you'll find the same pattern applies broadly. We covered a similar Groq-powered pipeline for cold outreach personalization from a CSV, and the classification-then-action architecture mirrors what you'd build for an on-call incident summarizer. The skill that compounds is knowing when to split work across models—fast/cheap for classification, capable/instruction-following for generation.
Common Pitfalls
Label leak: processed emails keep getting re-processed. The -label:PROCESSED Gmail query is case-sensitive. If you created the label as Processed, the query won't match. Stick to the all-caps convention used in the code.
Groq returns markdown-fenced JSON. Llama models sometimes wrap JSON in json fences even when instructed not to. The `.replace(/json|```/g, '')line handles this, but if you see parse errors, log the rawcontent` string to spot unexpected formatting.
Gemini rate limits. The free tier allows 15 RPM. If you process 10 emails per run every 5 minutes, you're at 2 RPM average—well within limits. But if you lower the trigger interval, watch for 429 responses. Implement exponential backoff if you scale up.
Plain body extraction misses formatted content. getPlainBody() strips HTML but can leave artifacts. For cleaner input, consider using getBody() and stripping HTML tags with a simple regex: body.replace(/<[^>]*>/g, ''). This gives the LLM cleaner context.
Drafts pile up on long threads. If a thread gets 20 replies, each new message triggers a new draft. This is usually desirable (each draft is context-aware to the latest message), but if you want only one draft per thread, check for existing drafts before creating a new one.
FAQ
Q: Why use two different LLMs instead of just Gemini for everything?
A: Groq's Llama 3.1 70B is significantly faster for simple classification (often sub-300ms) and has a more generous free tier for high-throughput tasks. Gemini 1.5 Flash is better at nuanced instruction-following for reply drafting. Splitting the workload gives you speed where it matters and quality where it counts, all within free limits.
Q: Can I use this on a Google Workspace account?
A: Yes. Apps Script runs identically on consumer Gmail and Workspace accounts. If you're on a Workspace plan, your admin may need to enable Apps Script execution, but it's typically allowed by default.
Q: How do I stop the agent from drafting replies to my own sent emails?
A: Add a filter in the Gmail query: in:inbox -label:PROCESSED -from:me. This excludes any thread where your own address is the latest sender.
Q: Will this work on mobile?
A: The agent runs server-side in Google's infrastructure, not on your device. Drafts and labels appear in Gmail everywhere—web, iOS, Android. You only interact with the output.
Q: What happens if an email body contains sensitive data?
A: Both Groq and Gemini API calls transmit data to their respective servers. Review each provider's data usage policy. For highly sensitive inboxes, consider running a local model via Ollama, but that moves you beyond the free-tier scope of this guide.
Q: I'm preparing for roles where I'd build and deploy exactly this kind of agent. What should I focus on demonstrating?
A: The pattern that impresses is not just the code—it's the operational thinking: idempotency with the PROCESSED label, error handling that doesn't block the queue, and splitting work across models for cost/latency optimization. If you're building a portfolio to demonstrate deployment velocity, the FDE portfolio guide walks through what hiring managers actually look for in projects like this.
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