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
- Event preparation - Anticipate and prepare before events happen
- Multi-source context - CRM, support, Slack, email
- Timely delivery - 30-60 minutes before meeting
- Actionable output - Talking points, questions, risks, opportunities
- 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.