Every morning I used to open my inbox and find myself writing variations of the same five replies — inquiries, collaboration requests, technical questions. The content varied, but the patterns didn't. That realization led me to build a simple system using Claude API and Google Apps Script (GAS) that now handles the first pass of my email triage automatically.
GAS runs on Google's servers for free, integrates with Gmail out of the box, and adding Claude API on top gives you AI-powered classification and draft generation without spinning up any infrastructure. If you're managing a small business, indie app, or content site and find email eating into your creative time, this setup is worth the two-hour investment to get running.
Why Claude API + Google Apps Script
There are plenty of email automation tools — Zapier, Make, n8n. Most charge a monthly fee, and the visual workflow editors get complicated fast once you need custom logic.
The GAS + Claude API combination works well for three reasons. First, GAS runs serverlessly on Google's infrastructure — no hosting costs, no maintenance, and you can trigger it hourly without thinking about it. Second, Gmail's GAS APIs are genuinely excellent: fetching threads, applying labels, and creating drafts all take a few lines of code. Third, Claude API costs are manageable for email volume. Processing 30–50 emails per day with Haiku 4.5 typically costs a few dollars per month at most, per the Claude API pricing guide.
Setup — API Key and GAS Project
Get your Claude API key:
Log in to the Anthropic Console, navigate to "API Keys," and generate a new key. It starts with sk-ant-....
Create your GAS project:
- Go to script.google.com and create a new project
- Open "⚙️ Project Settings" → "Script Properties" and add
CLAUDE_API_KEYwith your key as the value
Storing the key in Script Properties keeps it out of your code and version history.
Fetching Emails and Sending Them to Claude
Here's the entry point — fetching unread messages and routing them through the pipeline:
/**
* Fetch unread emails and process them with Claude
* Recommended GAS trigger: time-based, every hour
*/
function processUnreadEmails() {
const apiKey = PropertiesService.getScriptProperties()
.getProperty('CLAUDE_API_KEY');
// Fetch up to 10 unread threads from the last 24 hours
const threads = GmailApp.search('is:unread newer_than:1d', 0, 10);
if (threads.length === 0) {
console.log('No unread emails');
return;
}
threads.forEach(thread => {
const messages = thread.getMessages();
const latestMessage = messages[messages.length - 1];
// Skip automated emails (newsletters, notifications)
const from = latestMessage.getFrom();
if (isAutoSentEmail(from)) {
latestMessage.markRead();
return;
}
const subject = latestMessage.getSubject();
const body = latestMessage.getPlainBody().slice(0, 2000);
const result = classifyAndDraftReply(apiKey, subject, body, from);
applyLabelAndCreateDraft(thread, latestMessage, result);
latestMessage.markRead();
});
}
/**
* Basic check to skip automated sender addresses
*/
function isAutoSentEmail(from) {
const autoPatterns = [
'noreply', 'no-reply', 'donotreply',
'notification', 'newsletter', 'mailer-daemon'
];
return autoPatterns.some(pattern =>
from.toLowerCase().includes(pattern)
);
}Classifying and Drafting Replies with Claude
The key design decision here is to do classification and draft generation in a single API call. This cuts latency in half and keeps costs low.
/**
* Call Claude API to classify the email and generate a reply draft
* @returns {Object} { category, priority, summary, draftReply }
*/
function classifyAndDraftReply(apiKey, subject, body, from) {
const prompt = `
Analyze the following email and respond in JSON format only.
From: ${from}
Subject: ${subject}
Body:
${body}
Respond with this exact JSON structure:
{
"category": "inquiry|collaboration|technical|spam|other",
"priority": "high|medium|low",
"summary": "One sentence summary (under 20 words)",
"draftReply": "Professional reply draft (under 150 words)"
}
Category definitions:
- inquiry: questions about products, services, or pricing
- collaboration: partnership proposals, guest posts, sponsorships
- technical: bug reports, technical questions
- spam: unsolicited sales, irrelevant promotions
- other: anything else
`;
const payload = {
model: 'claude-haiku-4-5-20251001',
max_tokens: 1024,
messages: [
{ role: 'user', content: prompt }
]
};
const options = {
method: 'post',
contentType: 'application/json',
headers: {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01'
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(
'https://api.anthropic.com/v1/messages',
options
);
if (response.getResponseCode() !== 200) {
console.error('API error:', response.getContentText());
return { category: 'other', priority: 'low', draftReply: '' };
}
const data = JSON.parse(response.getContentText());
const content = data.content[0].text;
// Extract JSON even if Claude wraps it in a code block
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
return { category: 'other', priority: 'low', draftReply: '' };
}
return JSON.parse(jsonMatch[0]);
} catch (error) {
console.error('Processing error:', error.toString());
return { category: 'other', priority: 'low', draftReply: '' };
}
}Using claude-haiku-4-5-20251001 here is intentional. Email triage doesn't require deep reasoning — it needs consistency and speed. Haiku delivers both at roughly one-fifth the cost of Sonnet, and in practice the classification accuracy is nearly identical for this task.
Applying Labels and Creating Drafts in Gmail
This function takes Claude's output and writes it back into Gmail:
/**
* Apply labels based on classification and create a reply draft
*/
function applyLabelAndCreateDraft(thread, message, result) {
const { category, priority, summary, draftReply } = result;
// Get or create the category label
const labelName = `AI-Classified/${category}`;
let label = GmailApp.getUserLabelByName(labelName);
if (!label) {
label = GmailApp.createLabel(labelName);
}
thread.addLabel(label);
// Flag high-priority emails separately
if (priority === 'high') {
const urgentLabel = GmailApp.getUserLabelByName('AI-Classified/⭐ Needs Action')
|| GmailApp.createLabel('AI-Classified/⭐ Needs Action');
thread.addLabel(urgentLabel);
thread.markImportant();
}
// Create draft reply (skip spam)
if (category !== 'spam' && draftReply) {
const replyBody = `${draftReply}\n\n---\n[AI draft | ${summary}]`;
GmailApp.createDraft(
message.getFrom(),
`Re: ${message.getSubject()}`,
replyBody,
{
threadId: thread.getId(),
inReplyTo: message.getId()
}
);
}
console.log(`Processed: ${category} / ${priority} / ${summary}`);
}After running, you'll see an AI-Classified label group in your Gmail sidebar with sub-labels for each category. Drafts land in your Drafts folder attached to the original thread — review, edit, send.
Three Gotchas Worth Knowing About
1. GAS execution time limit
GAS scripts time out at 6 minutes. If your inbox gets heavy and Claude responses are slow, processing more than 10 threads per run risks hitting this limit. Keep the batch size at 10 or fewer and let the hourly trigger handle the queue.
2. JSON extraction from Claude's response
Claude reliably returns JSON, but occasionally wraps it in a markdown code block (```json ... ```). The regex content.match(/\{[\s\S]*\}/) extracts the raw JSON regardless of formatting, which prevents silent failures.
3. Rate limit collisions
If multiple GAS triggers fire in quick succession (which can happen with overlapping time-based triggers), you may hit Claude's rate limits. The Claude API rate limit guide covers handling 429 errors, but the simpler fix is to set a single hourly trigger and store a lastRun timestamp in Script Properties as a guard against double-execution.
Start Small Before Automating
Run the script manually from the GAS editor first. Check that the labels appear correctly and that the draft replies make sense for your email style. Once the output looks good, add a time-based trigger from "Triggers" → "Add Trigger" and set it to run every hour.
The AI-generated drafts aren't meant to go out as-is — they're a starting point. You still read and send. What changes is that you're editing a 90% complete reply instead of writing from scratch, which cuts the time per email from 5 minutes to under 30 seconds for routine messages.