Blueprint/Chapter 11
Chapter 11

Pattern 3: Event Preparation

By Zavier SandersSeptember 21, 2025

Build a meeting briefing agent that prepares context before important events.

Prefer something you can ship today? Start with theQuickstart: Ship One Agent with Mastra— then come back here to deepen the concepts.

Pattern Overview

Event preparation agents anticipate upcoming events and prepare relevant context in advance.

The Event Preparation Flow

┌─────────────────────────────────────────────────────────────┐
│           EVENT PREPARATION PATTERN                         │
│                                                             │
│  1. Monitor Calendar/Schedule                              │
│     • Upcoming meetings                                    │
│     • Deadlines                                            │
│     • Appointments                                         │
│     • Events                                               │
│                                                             │
│            ▼                                                │
│  2. Gather Context                                         │
│     • Meeting participants                                 │
│     • Previous interactions                                │
│     • Related documents                                    │
│     • Recent activity                                      │
│                                                             │
│            ▼                                                │
│  3. Generate Briefing                                      │
│     • Summarize context                                    │
│     • Key points                                           │
│     • Action items                                         │
│     • Talking points                                       │
│                                                             │
│            ▼                                                │
│  4. Deliver Before Event                                   │
│     • Email reminder                                       │
│     • Slack message                                        │
│     • Calendar note                                        │
│     • Mobile notification                                  │
└─────────────────────────────────────────────────────────────┘

Common Use Cases

Meeting Preparation:

  • Sales calls → Gather CRM data → Pre-call briefing
  • Interviews → Candidate research → Interviewer guide
  • Board meetings → Collect metrics → Executive summary

Event Preparation:

  • Conference → Speaker bios → Attendee briefing
  • Client visit → Account history → Team briefing
  • Product launch → Market analysis → Launch brief

Deadline Preparation:

  • Report due → Gather data → Draft outline
  • Review meeting → Collect feedback → Summary
  • Sprint planning → Previous sprint → Retrospective brief

Example: Meeting Briefing Agent

The Problem

Your sales team has 5-10 customer calls per day. Before each call, they need:sse4sxx 7e4

  • Customer account history
  • Recent support tickets
  • Product usage data
  • Previous conversation notes
  • Key stakeholders

Current process: Sales rep scrambles through multiple systems 10 minutes before each call, often missing important context.

What we'll build: An AI agent that monitors your calendar, gathers relevant context, and delivers a comprehensive briefing 30 minutes before each meeting.

Architecture

┌─────────────────────────────────────────────────────────────┐
│         MEETING BRIEFING AGENT ARCHITECTURE                 │
│                                                             │
│  Google Calendar Sync                                       │
│  (Check every 15 minutes)                                   │
│       │                                                     │
│       ▼                                                     │
│  ┌──────────────────┐                                      │
│  │ Event Scanner    │                                      │
│  │                  │                                      │
│  │ • Find upcoming  │                                      │
│  │ • 30-60 min out  │                                      │
│  │ • Not briefed yet│                                      │
│  └────────┬─────────┘                                      │
│           │                                                 │
│           ▼                                                 │
│  ┌─────────────────────────┐                              │
│  │ Context Gatherer         │                              │
│  │                          │                              │
│  │ • CRM data (HubSpot)     │                              │
│  │ • Support tickets        │                              │
│  │ • Slack history          │                              │
│  │ • Email threads          │                              │
│  │ • Product usage          │                              │
│  └────────┬────────────────┘                               │
│           │                                                 │
│           ▼                                                 │
│  ┌─────────────────────────┐                              │
│  │  Briefing Generator      │                              │
│  │                          │                              │
│  │ • Synthesize context     │                              │
│  │ • Generate summary       │                              │
│  │ • Extract talking points │                              │
│  │ • Suggest questions      │                              │
│  └────────┬────────────────┘                               │
│           │                                                 │
│           ▼                                                 │
│  ┌─────────────────────────┐                              │
│  │  Delivery                │                              │
│  └────┬────────────────────┘                               │
│       │                                                     │
│   ┌───▼─────────┐                                          │
│   │Slack DM     │                                          │
│   │+ Calendar   │                                          │
│   └─────────────┘                                          │
└─────────────────────────────────────────────────────────────┘

Setup

Dependencies

npm install @mastra/core @ai-sdk/openai zod
npm install googleapis
npm install @hubspot/api-client
npm install @slack/web-api
npm install prisma @prisma/client

Environment Setup

# .env
OPENAI_API_KEY=sk-...

# Google Calendar
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GOOGLE_REFRESH_TOKEN=...

# HubSpot CRM
HUBSPOT_API_KEY=...

# Slack
SLACK_BOT_TOKEN=xoxb-...

# Database
DATABASE_URL=postgresql://...

Database Schema

model Meeting {
  id          String   @id @default(cuid())
  eventId     String   @unique
  title       String
  startTime   DateTime
  endTime     DateTime
  attendees   String[]
  location    String?
  briefed     Boolean  @default(false)
  briefingId  String?
  createdAt   DateTime @default(now())

  @@index([startTime, briefed])
}

model Briefing {
  id          String   @id @default(cuid())
  meetingId   String   @unique
  content     Json
  deliveredAt DateTime @default(now())
  deliveredTo String[]
}

Calendar Integration

Google Calendar Client

// lib/calendar.ts
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';

export class CalendarService {
  private oauth2Client: OAuth2Client;
  private calendar;

  constructor() {
    this.oauth2Client = new google.auth.OAuth2(
      process.env.GOOGLE_CLIENT_ID,
      process.env.GOOGLE_CLIENT_SECRET
    );

    this.oauth2Client.setCredentials({
      refresh_token: process.env.GOOGLE_REFRESH_TOKEN,
    });

    this.calendar = google.calendar({ version: 'v3', auth: this.oauth2Client });
  }

  async getUpcomingEvents(
    timeMin: Date,
    timeMax: Date
  ): Promise<CalendarEvent[]> {
    const response = await this.calendar.events.list({
      calendarId: 'primary',
      timeMin: timeMin.toISOString(),
      timeMax: timeMax.toISOString(),
      singleEvents: true,
      orderBy: 'startTime',
    });

    const events = response.data.items || [];

    return events.map((event) => ({
      id: event.id!,
      title: event.summary || 'Untitled',
      startTime: new Date(event.start?.dateTime || event.start?.date!),
      endTime: new Date(event.end?.dateTime || event.end?.date!),
      attendees:
        event.attendees?.map((a) => a.email!).filter(Boolean) || [],
      location: event.location,
      description: event.description,
    }));
  }

  async findMeetingsNeedingBriefing(): Promise<CalendarEvent[]> {
    const now = new Date();
    const lookAhead = new Date(now.getTime() + 60 * 60 * 1000); // 1 hour

    // Get events in next hour
    const events = await this.getUpcomingEvents(now, lookAhead);

    // Filter to those needing briefings (30+ minutes away, not briefed)
    const needsBriefing = [];
    
    for (const event of events) {
      const minutesUntil = (event.startTime.getTime() - now.getTime()) / 60000;

      // Between 30-60 minutes away
      if (minutesUntil >= 30 && minutesUntil <= 60) {
        const existing = await db.meeting.findUnique({
          where: { eventId: event.id },
        });

        if (!existing || !existing.briefed) {
          needsBriefing.push(event);
        }
      }
    }

    return needsBriefing;
  }
}

Context Gathering

Multi-Source Context Collector

// lib/context-gatherer.ts
import { HubSpotClient } from '@hubspot/api-client';
import { WebClient } from '@slack/web-api';

export class ContextGatherer {
  private hubspot: HubSpotClient;
  private slack: WebClient;

  constructor() {
    this.hubspot = new HubSpotClient({ accessToken: process.env.HUBSPOT_API_KEY });
    this.slack = new WebClient(process.env.SLACK_BOT_TOKEN);
  }

  async gatherForMeeting(meeting: CalendarEvent): Promise<MeetingContext> {
    const attendeeEmails = meeting.attendees.filter(
      (email) => !email.includes('@yourcompany.com')
    );

    const [crmData, supportTickets, slackHistory, emailThreads] =
      await Promise.allSettled([
        this.getCRMData(attendeeEmails),
        this.getSupportTickets(attendeeEmails),
        this.getSlackHistory(attendeeEmails),
        this.getEmailThreads(attendeeEmails),
      ]);

    return {
      meeting,
      crm: crmData.status === 'fulfilled' ? crmData.value : null,
      support: supportTickets.status === 'fulfilled' ? supportTickets.value : [],
      slack: slackHistory.status === 'fulfilled' ? slackHistory.value : [],
      emails: emailThreads.status === 'fulfilled' ? emailThreads.value : [],
    };
  }

  private async getCRMData(emails: string[]): Promise<CRMData[]> {
    const contacts = await Promise.all(
      emails.map(async (email) => {
        try {
          const response = await this.hubspot.crm.contacts.searchApi.doSearch({
            filterGroups: [{
              filters: [{ propertyName: 'email', operator: 'EQ', value: email }],
            }],
            properties: [
              'firstname',
              'lastname',
              'company',
              'jobtitle',
              'phone',
              'lifecyclestage',
              'hs_lead_status',
            ],
          });

          if (response.results.length === 0) return null;

          const contact = response.results[0];
          
          // Get recent deals
          const deals = await this.hubspot.crm.deals.searchApi.doSearch({
            filterGroups: [{
              filters: [{
                propertyName: 'associations.contact',
                operator: 'EQ',
                value: contact.id,
              }],
            }],
            properties: ['dealname', 'amount', 'dealstage', 'closedate'],
            limit: 5,
          });

          return {
            email,
            name: `${contact.properties.firstname} ${contact.properties.lastname}`,
            company: contact.properties.company,
            title: contact.properties.jobtitle,
            phone: contact.properties.phone,
            stage: contact.properties.lifecyclestage,
            deals: deals.results.map((d) => ({
              name: d.properties.dealname,
              amount: d.properties.amount,
              stage: d.properties.dealstage,
            })),
          };
        } catch (error) {
          console.error(`Failed to fetch CRM data for ${email}:`, error);
          return null;
        }
      })
    );

    return contacts.filter(Boolean) as CRMData[];
  }

  private async getSupportTickets(emails: string[]): Promise<SupportTicket[]> {
    // Implementation depends on your support system
    // Example: Zendesk, Intercom, etc.
    const tickets = [];

    for (const email of emails) {
      const response = await fetch(
        `https://api.zendesk.com/api/v2/search.json?query=requester:${email}`,
        {
          headers: {
            Authorization: `Bearer ${process.env.ZENDESK_API_KEY}`,
          },
        }
      );

      const data = await response.json();
      
      tickets.push(
        ...data.results.map((t: any) => ({
          id: t.id,
          subject: t.subject,
          status: t.status,
          priority: t.priority,
          createdAt: new Date(t.created_at),
          description: t.description,
        }))
      );
    }

    return tickets.slice(0, 10); // Most recent 10
  }

  private async getSlackHistory(emails: string[]): Promise<SlackMessage[]> {
    // Find Slack users by email
    const users = await Promise.all(
      emails.map(async (email) => {
        try {
          const result = await this.slack.users.lookupByEmail({ email });
          return result.user?.id;
        } catch {
          return null;
        }
      })
    );

    const validUsers = users.filter(Boolean);
    if (validUsers.length === 0) return [];

    // Get recent DMs
    const messages: SlackMessage[] = [];

    for (const userId of validUsers) {
      try {
        const conversations = await this.slack.conversations.open({
          users: userId!,
        });

        const channelId = conversations.channel?.id;
        if (!channelId) continue;

        const history = await this.slack.conversations.history({
          channel: channelId,
          limit: 20,
        });

        messages.push(
          ...(history.messages || []).map((msg) => ({
            text: msg.text || '',
            timestamp: new Date(parseFloat(msg.ts!) * 1000),
            user: userId!,
          }))
        );
      } catch (error) {
        console.error(`Failed to fetch Slack history for ${userId}:`, error);
      }
    }

    return messages;
  }

  private async getEmailThreads(emails: string[]): Promise<EmailThread[]> {
    // Implementation depends on your email system (Gmail, Outlook, etc.)
    // This is a simplified example
    return [];
  }
}

Dossier Generation

Briefing Generator Agent

// src/mastra/agents/briefing-generator.ts
import { Agent } from '@mastra/core';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

const BriefingSchema = z.object({
  summary: z.string().describe('2-3 paragraph executive summary'),
  keyPoints: z.array(z.string()).describe('5-7 critical points to know'),
  talkingPoints: z.array(z.string()).describe('Suggested topics to discuss'),
  questions: z.array(z.string()).describe('Questions to ask'),
  risks: z.array(z.string()).describe('Potential issues or concerns'),
  opportunities: z.array(z.string()).describe('Upsell or engagement opportunities'),
  actionItems: z.array(z.string()).describe('Things to follow up on'),
});

export type Briefing = z.infer<typeof BriefingSchema>;

export const briefingAgent = new Agent({
  name: 'Meeting Briefing Generator',
  instructions: `You are an expert executive assistant preparing briefings for important meetings.

# YOUR ROLE
Synthesize information from multiple sources into a concise, actionable briefing.

# WHAT TO INCLUDE
- **Summary**: Context about attendees, recent history, current status
- **Key Points**: Most important facts (account status, open issues, opportunities)
- **Talking Points**: What to discuss in the meeting
- **Questions**: Smart questions to ask to advance the relationship
- **Risks**: Things that could go wrong or concerns to address
- **Opportunities**: Upsell, expansion, or engagement opportunities
- **Action Items**: Follow-ups needed after the meeting

# GUIDELINES
- Be concise but comprehensive
- Focus on actionable insights
- Highlight urgent items
- Include specific data (numbers, dates, names)
- Call out anything unusual or requiring attention`,
  model: {
    provider: 'OPEN_AI',
    name: 'gpt-4o',
    temperature: 0.3,
  },
});

export async function generateBriefing(
  context: MeetingContext
): Promise<Briefing> {
  const { meeting, crm, support, slack, emails } = context;

  const prompt = `Generate a meeting briefing for:

**Meeting Details**
Title: ${meeting.title}
Time: ${meeting.startTime.toLocaleString()}
Duration: ${Math.round((meeting.endTime.getTime() - meeting.startTime.getTime()) / 60000)} minutes
Location: ${meeting.location || 'Not specified'}

**Attendees**
${meeting.attendees.join(', ')}

**CRM Data**
${crm?.map((c) => `
Name: ${c.name}
Company: ${c.company}
Title: ${c.title}
Stage: ${c.stage}
Recent Deals: ${c.deals.map(d => `${d.name} ($${d.amount}) - ${d.stage}`).join(', ')}
`).join('\n\n') || 'No CRM data available'}

**Recent Support Tickets**
${support?.slice(0, 5).map((t) => `
#${t.id}: ${t.subject}
Status: ${t.status} | Priority: ${t.priority}
Created: ${t.createdAt.toLocaleDateString()}
`).join('\n') || 'No recent tickets'}

**Recent Slack Conversations**
${slack?.slice(0, 10).map((m) => `
[${m.timestamp.toLocaleString()}] ${m.text}
`).join('\n') || 'No recent Slack history'}

**Email Context**
${emails?.length || 0} email threads found

Generate a comprehensive briefing.`;

  const result = await briefingAgent.generate(prompt, {
    output: BriefingSchema,
  });

  return result.object;
}

Delivery

Slack Delivery

// lib/delivery.ts
import { WebClient } from '@slack/web-api';

const slack = new WebClient(process.env.SLACK_BOT_TOKEN);

export async function deliverBriefing(
  meeting: CalendarEvent,
  briefing: Briefing,
  userId: string
): Promise<void> {
  const blocks = [
    {
      type: 'header',
      text: {
        type: 'plain_text',
        text: `📋 Briefing: ${meeting.title}`,
      },
    },
    {
      type: 'context',
      elements: [{
        type: 'mrkdwn',
        text: `🕐 ${meeting.startTime.toLocaleString()} | ${meeting.attendees.length} attendees`,
      }],
    },
    { type: 'divider' },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*Summary*\n${briefing.summary}`,
      },
    },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*Key Points*\n${briefing.keyPoints.map((p) => `${p}`).join('\n')}`,
      },
    },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*Talking Points*\n${briefing.talkingPoints.map((p) => `${p}`).join('\n')}`,
      },
    },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*Questions to Ask*\n${briefing.questions.map((q) => `${q}`).join('\n')}`,
      },
    },
  ];

  if (briefing.risks.length > 0) {
    blocks.push({
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*⚠️ Risks & Concerns*\n${briefing.risks.map((r) => `${r}`).join('\n')}`,
      },
    });
  }

  if (briefing.opportunities.length > 0) {
    blocks.push({
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*💡 Opportunities*\n${briefing.opportunities.map((o) => `${o}`).join('\n')}`,
      },
    });
  }

  await slack.chat.postMessage({
    channel: userId,
    text: `Briefing for ${meeting.title}`,
    blocks: blocks as any,
  });

  console.log(`📮 Delivered briefing for "${meeting.title}" to ${userId}`);
}

Calendar Note

// lib/calendar-update.ts
export async function addBriefingToCalendar(
  calendarService: CalendarService,
  eventId: string,
  briefing: Briefing
): Promise<void> {
  const briefingText = `
🤖 AI-Generated Briefing

${briefing.summary}

KEY POINTS:
${briefing.keyPoints.map((p) => `${p}`).join('\n')}

TALKING POINTS:
${briefing.talkingPoints.map((p) => `${p}`).join('\n')}

QUESTIONS:
${briefing.questions.map((q) => `${q}`).join('\n')}
`;

  await calendarService.updateEvent(eventId, {
    description: briefingText,
  });
}

Complete Pipeline

// app/api/cron/prepare-briefings/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { CalendarService } from '@/lib/calendar';
import { ContextGatherer } from '@/lib/context-gatherer';
import { generateBriefing } from '@/mastra/agents/briefing-generator';
import { deliverBriefing } from '@/lib/delivery';
import { db } from '@/lib/db';

export async function GET(request: NextRequest) {
  // Verify cron secret
  if (request.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const calendar = new CalendarService();
  const gatherer = new ContextGatherer();

  // Find meetings needing briefings (30-60 minutes away)
  const meetings = await calendar.findMeetingsNeedingBriefing();

  console.log(`Found ${meetings.length} meetings needing briefings`);

  let briefingsGenerated = 0;

  for (const meeting of meetings) {
    try {
      // Gather context
      console.log(`Gathering context for: ${meeting.title}`);
      const context = await gatherer.gatherForMeeting(meeting);

      // Generate briefing
      console.log(`Generating briefing...`);
      const briefing = await generateBriefing(context);

      // Store in database
      await db.meeting.upsert({
        where: { eventId: meeting.id },
        create: {
          eventId: meeting.id,
          title: meeting.title,
          startTime: meeting.startTime,
          endTime: meeting.endTime,
          attendees: meeting.attendees,
          location: meeting.location,
          briefed: true,
        },
        update: {
          briefed: true,
        },
      });

      const briefingRecord = await db.briefing.create({
        data: {
          meetingId: meeting.id,
          content: briefing as any,
          deliveredTo: meeting.attendees,
        },
      });

      // Deliver briefing (find meeting organizer's Slack ID)
      const organizerEmail = meeting.attendees[0]; // Simplified
      const slackUserId = await getSlackUserId(organizerEmail);
      
      if (slackUserId) {
        await deliverBriefing(meeting, briefing, slackUserId);
      }

      briefingsGenerated++;
    } catch (error) {
      console.error(`Failed to generate briefing for ${meeting.title}:`, error);
    }
  }

  return NextResponse.json({
    success: true,
    briefings: briefingsGenerated,
  });
}

async function getSlackUserId(email: string): Promise<string | null> {
  const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
  try {
    const result = await slack.users.lookupByEmail({ email });
    return result.user?.id || null;
  } catch {
    return null;
  }
}

Vercel Cron

// vercel.json
{
  "crons": [
    {
      "path": "/api/cron/prepare-briefings",
      "schedule": "*/15 * * * *"
    }
  ]
}

Key Takeaways

  1. Event preparation - Anticipate and prepare before events happen
  2. Multi-source context - CRM, support, Slack, email
  3. Timely delivery - 30-60 minutes before meeting
  4. Actionable output - Talking points, questions, risks, opportunities
  5. Automated pipeline - Calendar sync → Context gather → Generate → Deliver

Pattern 3 complete! Next: Pattern 4 (Scheduled Analysis).

Get chapter updates & code samples

We’ll email diagrams, code snippets, and additions.