Gmail Assistant
A complete example of an agent built on top of the Gmail Data Connector. It can summarize unread threads, draft replies, and auto-label new inbound mail — all without you writing a single line of OAuth code.
Overview
- Tier:
tool - Dependencies:
gmail(via Data Connector) - Triggers:
dataConnectorTriggerson new Gmail messages + a cron fallback - No OAuth manifest needed — the Data Connector owns the connection.
Manifest
json
{
"trigger": "auto",
"dataConnectors": ["gmail"],
"dataConnectorTriggers": [
{
"toolkit": "gmail",
"triggerSlug": "GMAIL_NEW_GMAIL_MESSAGE",
"config": { "labels": ["INBOX"] }
}
],
"automation": {
"triggers": [
{ "type": "webhook", "description": "Real-time on new Gmail message" },
{ "type": "cron", "default": "*/30 * * * *", "description": "Catch-up sweep" }
],
"timeout": 60
},
"tool": {
"name": "gmail_assistant",
"description": "Summarize, search, and reply to Gmail threads.",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["summarize_unread", "search", "draft_reply"]
},
"query": { "type": "string" },
"threadId": { "type": "string" },
"prompt": { "type": "string" }
},
"required": ["action"]
}
}
}Code
js
/**
* Gmail Assistant
*
* Reads, summarizes, labels, and drafts replies to Gmail messages.
* All Gmail access goes through context.dataConnectors — no OAuth needed.
*/
async function handler(input, context) {
const gmail = context.dataConnectors.get('gmail');
// Path 1: external trigger — a new Gmail message arrived.
if (input.triggeredBy === 'webhook' && input.triggerData?.triggerSlug === 'GMAIL_NEW_GMAIL_MESSAGE') {
return await handleNewMessage(gmail, context, input.triggerData.payload);
}
// Path 2: cron catch-up.
if (input.triggeredBy === 'cron') {
return await summarizeUnread(gmail, context);
}
// Path 3: direct tool call from chat.
switch (input.action) {
case 'summarize_unread':
return await summarizeUnread(gmail, context);
case 'search':
return await searchInbox(gmail, input.query);
case 'draft_reply':
return await draftReply(gmail, context, input.threadId, input.prompt);
default:
return { error: true, message: `Unknown action: ${input.action}` };
}
}
async function summarizeUnread(gmail, context) {
const { data } = await gmail.execute('GMAIL_FETCH_EMAILS', {
query: 'is:unread newer_than:1d',
max_results: 20,
});
const messages = data?.messages || [];
if (messages.length === 0) {
return { summary: 'No unread emails in the last day.', count: 0 };
}
const summary = await context.ai.chat({
system: 'You are a concise inbox summarizer.',
messages: [{
role: 'user',
content:
'Summarize these unread emails in 3-5 bullet points. Group by sender when helpful:\n\n' +
messages.map(m => `From: ${m.from}\nSubject: ${m.subject}\nPreview: ${m.snippet}`).join('\n---\n'),
}],
});
return { summary: summary.content, count: messages.length };
}
async function searchInbox(gmail, query) {
const { data } = await gmail.execute('GMAIL_FETCH_EMAILS', {
query: query || 'is:starred',
max_results: 25,
});
return {
results: (data?.messages || []).map(m => ({
id: m.id,
from: m.from,
subject: m.subject,
snippet: m.snippet,
})),
};
}
async function draftReply(gmail, context, threadId, prompt) {
if (!threadId) return { error: true, message: 'threadId is required' };
const { data: thread } = await gmail.execute('GMAIL_FETCH_THREAD', { thread_id: threadId });
const lastMessage = (thread?.messages || []).slice(-1)[0];
const reply = await context.ai.chat({
system: 'You are a helpful assistant drafting an email reply. Match the tone of the thread.',
messages: [{
role: 'user',
content: `Thread subject: ${lastMessage?.subject}\n\nLast message:\n${lastMessage?.snippet}\n\nInstructions: ${prompt || 'Write a friendly acknowledgement.'}`,
}],
});
const { data: draft } = await gmail.execute('GMAIL_CREATE_EMAIL_DRAFT', {
thread_id: threadId,
to: lastMessage?.from,
subject: `Re: ${lastMessage?.subject}`,
body: reply.content,
});
return { draftId: draft?.id, body: reply.content };
}
async function handleNewMessage(gmail, context, payload) {
const messageId = payload?.messageId || payload?.id;
if (!messageId) return { skipped: true };
const { data: message } = await gmail.execute('GMAIL_FETCH_MESSAGE_BY_ID', { message_id: messageId });
const classification = await context.ai.chat({
system: 'Classify this email into exactly one of: urgent, newsletter, personal, other. Reply with only the label.',
messages: [{
role: 'user',
content: `From: ${message?.from}\nSubject: ${message?.subject}\nBody: ${message?.snippet}`,
}],
});
const label = (classification.content || 'other').trim().toLowerCase();
if (label === 'urgent') {
await gmail.execute('GMAIL_ADD_LABEL_TO_EMAIL', {
message_id: messageId,
label_names: ['Urgent'],
});
}
return { classified: label, messageId };
}What Makes This Work
"dataConnectors": ["gmail"]— tells PIE this agent needs Gmail. At install time, PIE refuses to install until the user has connected Gmail (reusing an existing connection if the user has one)."dataConnectorTriggers"— PIE subscribes the user to Gmail'sGMAIL_NEW_GMAIL_MESSAGEevent when they install. When the event fires, it's delivered to this agent viainput.triggerData.context.dataConnectors.get('gmail')— returns a scoped handle. No tokens, no refresh logic, no redirect URIs.handle.execute(action, params)— runs the action against the user's Gmail. If you declareddataConnectors: ["gmail"](toolkit-level), every Gmail action is available. If you used action-level scoping, only the listed actions are.
Variants
- Read-only agent: replace the manifest declaration with
dataConnectors: [{ toolkit: "gmail", actions: ["GMAIL_FETCH_EMAILS", "GMAIL_FETCH_THREAD"] }]. - Multi-provider (Gmail + Slack): add
"slack"todataConnectorsand usecontext.dataConnectors.get('slack')alongside the Gmail handle to cross-post thread summaries. - No triggers, just chat: drop
dataConnectorTriggersand theautomationblock — the agent will only run when the user calls it from chat.