GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-03-29Intermediate

Building an Intelligent Email Classification System with Gemini API — Function Calling and Structured Output in Practice

Learn how to use Gemini API's Function Calling and structured output to build a system that automatically classifies, summarizes, and prioritizes incoming emails — with working TypeScript code.

gemini-api277function-calling20structured-output22email-automationtypescript15

Why You Need Automated Email Classification

Sifting through a crowded inbox to distinguish between urgent requests, actionable tasks, and FYI messages takes more cognitive energy than you might think. By combining Gemini API's Function Calling with structured output, you can build an intelligent system that automatically classifies incoming emails, assigns priorities, and extracts action items.

This is a sample of the kind of practical system-building content available to Gemini Lab members. If you find this useful, consider joining our membership for more in-depth technical guides.

System Architecture Design

The email classification system consists of three stages:

Stage 1: Classification — Determine the email's category and priority level

Stage 2: Summarization — Extract key points in three sentences or fewer

Stage 3: Action Extraction — Generate action items when a response or follow-up is needed

// types/email.ts
export interface EmailInput {
  id: string;
  from: string;
  to: string;
  subject: string;
  body: string;
  receivedAt: string;
}
 
export type EmailCategory =
  | "urgent"       // Requires immediate attention
  | "action"       // Needs response (not urgent)
  | "info"         // FYI / informational
  | "newsletter"   // Newsletters / marketing
  | "spam";        // Spam / unwanted
 
export type Priority = "high" | "medium" | "low";
 
export interface ClassifiedEmail extends EmailInput {
  category: EmailCategory;
  priority: Priority;
  summary: string;
  actionItems: string[];
  confidence: number;   // Classification confidence (0-1)
}

Classifying Emails with Function Calling

Gemini's Function Calling lets you define a strict output schema for email classification. This ensures type-safe data handling in downstream processing.

import { GoogleGenerativeAI } from "@google/generative-ai";
 
const genAI = new GoogleGenerativeAI("YOUR_GEMINI_API_KEY");
 
// Email classification tool definition
const classifyEmailTool = {
  functionDeclarations: [{
    name: "classify_email",
    description: "Analyze an incoming email and return its category, priority, summary, and action items",
    parameters: {
      type: "object",
      properties: {
        category: {
          type: "string",
          enum: ["urgent", "action", "info", "newsletter", "spam"],
          description: "Email category",
        },
        priority: {
          type: "string",
          enum: ["high", "medium", "low"],
          description: "Priority level",
        },
        summary: {
          type: "string",
          description: "Summary of the email body (3 sentences max)",
        },
        action_items: {
          type: "array",
          items: { type: "string" },
          description: "List of required actions (empty array if none needed)",
        },
        confidence: {
          type: "number",
          description: "Classification confidence score (0.0-1.0)",
        },
      },
      required: ["category", "priority", "summary", "action_items", "confidence"],
    },
  }],
};
 
async function classifyEmail(email: EmailInput): Promise<ClassifiedEmail> {
  const model = genAI.getGenerativeModel({
    model: "gemini-2.5-flash",
    tools: [classifyEmailTool],
  });
 
  const prompt = `Analyze the following email:
 
From: ${email.from}
Subject: ${email.subject}
Body:
${email.body}
 
Classify this email using the classify_email function.`;
 
  const result = await model.generateContent(prompt);
  const response = result.response;
 
  // Extract the Function Call result
  const functionCall = response.candidates?.[0]?.content?.parts?.find(
    (part) => part.functionCall
  )?.functionCall;
 
  if (!functionCall || functionCall.name !== "classify_email") {
    throw new Error("Expected classify_email function call");
  }
 
  const args = functionCall.args as {
    category: EmailCategory;
    priority: Priority;
    summary: string;
    action_items: string[];
    confidence: number;
  };
 
  return {
    ...email,
    category: args.category,
    priority: args.priority,
    summary: args.summary,
    actionItems: args.action_items,
    confidence: args.confidence,
  };
}

By defining the schema in functionDeclarations, Gemini returns data that conforms to this structure. The enum constraints on categories ensure no unexpected values slip through.

Batch Processing for High-Volume Inboxes

When you have hundreds of emails to process, handling them one by one is too slow. Implement batch processing with concurrency control and rate limiting.

// utils/batch-classifier.ts
 
// Semaphore for concurrency limiting
class Semaphore {
  private queue: (() => void)[] = [];
  private running = 0;
 
  constructor(private max: number) {}
 
  async acquire(): Promise<void> {
    if (this.running < this.max) {
      this.running++;
      return;
    }
    return new Promise<void>((resolve) => {
      this.queue.push(resolve);
    });
  }
 
  release(): void {
    this.running--;
    const next = this.queue.shift();
    if (next) {
      this.running++;
      next();
    }
  }
}
 
async function batchClassify(
  emails: EmailInput[],
  concurrency = 5
): Promise<ClassifiedEmail[]> {
  const semaphore = new Semaphore(concurrency);
  const results: ClassifiedEmail[] = [];
  const errors: Array<{ email: EmailInput; error: string }> = [];
 
  const tasks = emails.map(async (email) => {
    await semaphore.acquire();
    try {
      const classified = await classifyEmail(email);
      results.push(classified);
    } catch (error) {
      errors.push({
        email,
        error: error instanceof Error ? error.message : "Unknown error",
      });
    } finally {
      semaphore.release();
    }
  });
 
  await Promise.all(tasks);
 
  if (errors.length > 0) {
    console.warn(`${errors.length}/${emails.length} emails failed classification`);
  }
 
  // Sort by priority (high → medium → low)
  const priorityOrder = { high: 0, medium: 1, low: 2 };
  results.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
 
  return results;
}

Customizing Classification Rules

Add a configuration layer to adapt classification behavior to different business needs and personal workflows.

// config/classification-rules.ts
interface ClassificationRules {
  urgentSenders: string[];      // Always treat as urgent
  infoOnlyDomains: string[];    // Always treat as info
  spamKeywords: string[];       // Keywords that trigger spam classification
  customPromptRules: string;    // Additional rules in natural language
}
 
const defaultRules: ClassificationRules = {
  urgentSenders: [
    "boss@company.com",
    "ceo@company.com",
  ],
  infoOnlyDomains: [
    "noreply@github.com",
    "notifications@slack.com",
  ],
  spamKeywords: [
    "unsubscribe",
    "limited time offer",
  ],
  customPromptRules: `
    - Client billing emails should always be "urgent" + "high"
    - Internal weekly reports should be "info" + "low"
    - Recruiting-related emails should be "action" + "medium"
  `,
};
 
function buildClassificationPrompt(
  email: EmailInput,
  rules: ClassificationRules
): string {
  // Pre-rule checks
  if (rules.urgentSenders.includes(email.from)) {
    return `This email is from an important sender (${email.from}). Classify it as "urgent".\n\n${email.body}`;
  }
 
  const domain = email.from.split("@")[1];
  if (domain && rules.infoOnlyDomains.includes(domain)) {
    return `This email is from a notification domain (${domain}). Classify it as "info".\n\n${email.body}`;
  }
 
  return `Follow these additional rules when classifying:
${rules.customPromptRules}
 
From: ${email.from}
Subject: ${email.subject}
Body: ${email.body}`;
}

Generating a Classification Dashboard

Create a human-readable report from the classification results.

function generateReport(emails: ClassifiedEmail[]): string {
  const byCategory = emails.reduce((acc, email) => {
    acc[email.category] = (acc[email.category] || 0) + 1;
    return acc;
  }, {} as Record<string, number>);
 
  const urgent = emails.filter(e => e.category === "urgent");
  const actions = emails.filter(e => e.category === "action");
 
  let report = `Email Classification Report\n`;
  report += `============================\n`;
  report += `Total: ${emails.length} emails\n\n`;
 
  Object.entries(byCategory).forEach(([cat, count]) => {
    const icon = { urgent: "RED", action: "YELLOW", info: "BLUE", newsletter: "NEWS", spam: "SPAM" }[cat] || "-";
    report += `[${icon}] ${cat}: ${count}\n`;
  });
 
  if (urgent.length > 0) {
    report += `\n[URGENT] Requires immediate attention:\n`;
    urgent.forEach(e => {
      report += `  - [${e.from}] ${e.subject}\n    ${e.summary}\n`;
    });
  }
 
  if (actions.length > 0) {
    report += `\n[ACTION] Needs response:\n`;
    actions.forEach(e => {
      report += `  - [${e.from}] ${e.subject}\n`;
      e.actionItems.forEach(item => {
        report += `    -> ${item}\n`;
      });
    });
  }
 
  return report;
}
 
// Usage
const classified = await batchClassify(inboxEmails);
console.log(generateReport(classified));
// Expected output:
// Email Classification Report
// ============================
// Total: 50 emails
//
// [RED] urgent: 3
// [YELLOW] action: 12
// [BLUE] info: 20
// [NEWS] newsletter: 10
// [SPAM] spam: 5
//
// [URGENT] Requires immediate attention:
//   - [client@example.com] Change in delivery date
//     Client requesting the delivery date be moved up by 3 days. Response needed by Friday.

Looking back

By leveraging Gemini API's Function Calling, you can efficiently build a system that automatically classifies, summarizes, and extracts action items from incoming emails. Strict schema definitions ensure type-safe data flow, while the custom rules layer lets you flexibly adapt the system to your business needs.

For Gemini API basics, see our "[Gemini API Quickstart]((/articles/gemini-api/gemini-api-quickstart)". For Function Calling details, check out "[Complete Guide to Function Calling]((/articles/gemini-advanced/function-calling-guide)". If you're interested in streaming responses, "[Implementing Streaming Responses and Multi-Turn Chat]((/articles/gemini-api/streaming-and-chat)" is also a great resource.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-06-25
Gemini API × TypeScript Type-Safe AI Application Architecture — Integrating Zod Schemas, Structured Output, and Streaming
Type-safe AI applications with the Gemini API and TypeScript: Zod validation, Structured Output, streaming pipelines, and error handling that holds up in production.
API / SDK2026-05-09
Gemini API Returns 400 When You Set tools and responseSchema Together — Three Designs That Make Function Calling and Structured Output Coexist
You want function calling to fetch external data and a strict JSON shape for the final answer. Setting tools and responseSchema together returns 400. Here's why, plus three production-tested designs that make both work.
API / SDK2026-05-09
Build a Voice + Screen-Share AI Pair Programmer with the Gemini Live API in TypeScript
A practical playbook for wiring the Gemini Live API to getDisplayMedia and a microphone to build an over-the-shoulder AI pair programmer in TypeScript, with cost controls and the gotchas I hit in production.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →