Skip to content

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: dataConnectorTriggers on 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

  1. "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).
  2. "dataConnectorTriggers" — PIE subscribes the user to Gmail's GMAIL_NEW_GMAIL_MESSAGE event when they install. When the event fires, it's delivered to this agent via input.triggerData.
  3. context.dataConnectors.get('gmail') — returns a scoped handle. No tokens, no refresh logic, no redirect URIs.
  4. handle.execute(action, params) — runs the action against the user's Gmail. If you declared dataConnectors: ["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" to dataConnectors and use context.dataConnectors.get('slack') alongside the Gmail handle to cross-post thread summaries.
  • No triggers, just chat: drop dataConnectorTriggers and the automation block — the agent will only run when the user calls it from chat.

See Also

Built with VitePress