
Meeting Prep
- 58 installs
- 339 repo stars
- Updated August 4, 2026
- glebis/claude-skills
Prepare meeting notes by pulling Cal.com bookings, researching participants, auditing prior sessions from an Obsidian vault, and linking prep to session notes.
About
Fetches upcoming Cal.com bookings, researches each participant, mines vault history, and writes structured prep notes (solo or group) into Obsidian. A developer uses it to prep for upcoming calls and later link prep notes to their post-meeting session notes.
- Config-driven; parallel participant research and Cal.com sync
- Link, sync, and status modes with ready-to-paste Dataview queries
Meeting Prep by the numbers
- 58 all-time installs (skills.sh)
- Ranked #1,566 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/glebis/claude-skills --skill meeting-prepAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 58 |
|---|---|
| repo stars | ★ 339 |
| Last updated | August 4, 2026 |
| Repository | glebis/claude-skills ↗ |
What it does
Prepare meeting notes by pulling Cal.com bookings, researching participants, auditing prior sessions from an Obsidian vault, and linking prep to session notes.
Files
Meeting Prep
Prepares comprehensive meeting prep notes for upcoming Cal.com bookings by researching participants and auditing session history from the Obsidian vault.
Configuration
All paths and settings are in config.yaml next to this file. Read it before executing any steps.
# config.yaml — key fields:
vault.root # Obsidian vault root
vault.meetings # Where meeting notes live
vault.people_search_depth # How deep to search for person notes
prep_notes.prefix # Filename prefix (default: "prep")
prep_notes.type_tag # Frontmatter type value (default: "meeting-prep")
calcom.lookahead_days # Default booking lookahead
calcom.timezone # Display timezone
calcom.time_format # 12h or 24hOn first run: Read config.yaml from the same directory as this SKILL.md file. Resolve ~ to the user's home directory. Use these values for all path references below — never hardcode paths.
When to Use
- User invokes
/meeting-prep - User asks to prepare for upcoming meetings/sessions/calls
- User says "who do I have coming up" or "prep for my next session"
Arguments
- No args → prep all bookings in the next
calcom.lookahead_daysdays - Date (
2026-06-03) → prep bookings on that specific date - Name (
Greg) → prep only bookings with that participant tomorrow/today/this week→ natural date rangeslink→ scan for unlinked prep↔session note pairs and connect themsync→ reconcile prep notes with Cal.com booking state (cancelled, rescheduled, deleted)status→ show Dataview-style overview of all prep notes and their link status
Workflow
Step 0: Load Config
Read config.yaml from the skill directory. Resolve all paths (expand ~). Store as variables for use in subsequent steps:
VAULT_ROOT = config.vault.root
MEETINGS_DIR = config.vault.meetings
PEOPLE_DEPTH = config.vault.people_search_depth
PREP_PREFIX = config.prep_notes.prefix
TYPE_TAG = config.prep_notes.type_tag
LOOKAHEAD = config.calcom.lookahead_days
TIMEZONE = config.calcom.timezoneStep 1: Fetch Bookings
Load Cal.com MCP tools via ToolSearch (query: "calcom", max_results: 20). Then:
1. Call get_bookings to retrieve upcoming bookings (status: accepted) within the target date range 2. For each booking, call get_booking_attendees to get the full attendee list — Cal.com bookings can have multiple attendees (e.g., lab sessions, masterminds, group calls) 3. Filter by arguments if provided (date, name) 4. If no bookings found, report and stop
Step 2: Search Vault for Session History
For each unique participant, search MEETINGS_DIR for previous session notes:
grep -rl "PARTICIPANT_NAME" $MEETINGS_DIR 2>/dev/nullRead matching files (up to 5 most recent) to extract:
- Date and duration of each past session
- Key topics discussed (from headings, summary sections, or transcript excerpts)
- Any action items or follow-ups mentioned
- Session source (Fathom, Granola, manual)
Also search for a person note in the vault:
find $VAULT_ROOT -maxdepth $PEOPLE_DEPTH -name "*PARTICIPANT_NAME*" -not -path "*/Meetings/*" 2>/dev/nullStep 3: Research Participants (Workflow)
Use the Workflow tool to parallelize participant research. Pass MEETINGS_DIR and VAULT_ROOT via workflow args so subagents know where to search.
For each unique participant, spawn an agent that:
1. Web-searches the participant's name + company/email domain 2. Checks LinkedIn profile (via web search, not direct scrape) 3. Summarizes: role, company, recent activity, mutual context 4. For returning participants: cross-references with vault history to identify patterns and continuity points
Step 4: Create Prep Notes in Obsidian
There are two modes depending on the number of attendees:
- Solo session (1 attendee): one prep note per session, exactly as described below. Filename:
YYYYMMDD-$PREP_PREFIX-PARTICIPANT_SLUG.md - Group session (2+ attendees): one prep note for the whole session. Filename:
YYYYMMDD-$PREP_PREFIX-EVENT_SLUG.md(whereEVENT_SLUGis the Cal.com event type slug in lowercase kebab-case). The note lists all attendees with brief profiles, and the research section covers all participants.
If a prep note already exists for that date+session, update it instead of creating a duplicate.
Solo Session Template
Use this template:
---
type: $TYPE_TAG
date: YYYY-MM-DD
participant: "Full Name"
email: email@example.com
event_type: "Cal.com event type name"
duration: X min
time: "HH:MM $TIMEZONE"
zoom_link: "https://..."
status: prep
session_note: ""
tags:
- $TYPE_TAG
- SESSION_TYPE
---
# Prep: Full Name — YYYY-MM-DD
## Session Info
- **Time**: HH:MM–HH:MM $TIMEZONE (Day of week)
- **Type**: Event type name (X min)
- **Location**: Zoom link
- **Participant timezone**: Timezone
## About FIRST_NAME
RESEARCH_SUMMARY
## Previous Sessions (N total)
### Most Recent: YYYY-MM-DD — Event Type
KEY_TOPICS_AND_OUTCOMES
### Earlier Sessions
BRIEF_LIST_WITH_DATES_AND_TOPICS
## Continuity & Follow-ups
- Outstanding action items from previous sessions
- Recurring themes or patterns
- Progress on previously discussed goals
## Prep Ideas
- Conversation starters from research
- Topics to revisit based on session history
- Questions to explore
## Notes
_Space for pre-meeting thoughts and during-meeting notes_Sequential Bookings for the Same Participant
When multiple bookings exist for the same participant within the lookahead window (e.g., Jun 3 and Jun 9), sort them by date and handle as follows:
- First booking: standard prep note, no changes.
- Second+ booking: the prep note should acknowledge the earlier session:
- Add
prior_prep: "[[YYYYMMDD-prep-participant]]"to frontmatter, pointing to the earlier prep note - Include a ## Prior Session section after Session Info:
## Prior Session
A session with FIRST_NAME is scheduled for PRIOR_DATE — it will have happened by the time this session occurs.
Update this note after the PRIOR_DATE session with fresh context.- Keep the full research/history sections but flag them as "will be stale after PRIOR_DATE" so the user knows to revisit
- The note is still useful as-is for background research, but action items and continuity will need a post-session refresh
For first-time participants (no vault history), replace the "Previous Sessions" and "Continuity" sections with:
## First Meeting
No previous sessions found in vault.
## Context & Talking Points
- Research-based conversation starters
- Questions to understand their goals and needs
- How they likely found you / booking contextGroup Session Template
For bookings with 2+ attendees, use this template instead:
---
type: $TYPE_TAG
date: YYYY-MM-DD
participants:
- name: "Full Name 1"
email: email1@example.com
- name: "Full Name 2"
email: email2@example.com
event_type: "Cal.com event type name"
duration: X min
time: "HH:MM $TIMEZONE"
zoom_link: "https://..."
status: prep
session_note: ""
tags:
- $TYPE_TAG
- SESSION_TYPE
---
# Prep: Event Type Name — YYYY-MM-DD
## Session Info
- **Time**: HH:MM–HH:MM $TIMEZONE (Day of week)
- **Type**: Event type name (X min)
- **Location**: Zoom link
- **Attendees**: N participants
## Attendees
### FIRST_NAME_1 LAST_NAME_1
BRIEF_PROFILE_AND_RESEARCH_SUMMARY
### FIRST_NAME_2 LAST_NAME_2
BRIEF_PROFILE_AND_RESEARCH_SUMMARY
## Previous Sessions per Attendee
SUMMARY_OF_VAULT_HISTORY_PER_ATTENDEE (grouped by person, skip if no history)
## Prep Ideas
- Group dynamics to consider
- Topics to cover with the full group
- Individual follow-ups to weave in
## Notes
_Space for pre-meeting thoughts and during-meeting notes_Step 5: Report
After creating all prep notes, output a concise summary:
- List of prepped sessions (date, time, participant, event type)
- Key highlights (first-timers flagged, notable follow-ups from history)
- Links to the created prep notes as
[[YYYYMMDD-PREP_PREFIX-participant-slug]]
Notes
- Always convert times to the configured
TIMEZONEfor display - Participant research should be respectful — professional context only
- If a participant has many previous sessions (>5), focus on the 3 most recent + overall patterns
- The prep note should be actionable: things to discuss, not just a dossier
- Tag first-time participants so they're easy to filter in Obsidian
- For group sessions, use the event type slug for the filename instead of participant name
Linking: meeting-prep link
When invoked with link, scan for prep notes that don't yet have a linked session note, and connect them.
How It Works
1. Find all prep notes:
grep -rl "type: $TYPE_TAG" $MEETINGS_DIR 2>/dev/null2. For each prep note with session_note: "" (unlinked):
- Extract
dateandparticipantfrom frontmatter - Search for a matching post-meeting note by date + participant name:
find $MEETINGS_DIR -name "YYYYMMDD-*" -not -name "*$PREP_PREFIX*" | xargs grep -l "PARTICIPANT_NAME" 2>/dev/null- Fathom notes have
participants:in YAML frontmatter; Granola notes haveparticipants:too - Match by date prefix AND participant name appearing in the file
3. When a match is found, update both files:
In the prep note:
- Update frontmatter:
session_note: "[[YYYYMMDD-matching-note-filename]]" - Update
status: prep→status: done
In the session note:
- Append a
## Prep Notesection at the end (before any existing## See alsosection if present):
## Prep Note
- [[YYYYMMDD-PREP_PREFIX-participant-slug]]- If the session note already has a
## See alsosection, add the link there instead of creating a new section
4. Report which notes were linked and which prep notes are still unlinked (meeting hasn't happened yet or transcript not imported)
Matching Rules
- Date must match exactly (same YYYYMMDD prefix)
- Participant name match is fuzzy: check both full name and first name, case-insensitive
- Skip files that are themselves prep notes (
-$PREP_PREFIX-in filename) - If multiple session notes match the same date+participant, prefer the one with a transcript (longer file)
- Never create duplicate links — check if the link already exists before adding
Sync: meeting-prep sync
When invoked with sync, reconcile all active prep notes with the current state of their Cal.com bookings. This catches cancellations, reschedules, and deleted bookings that would otherwise leave stale prep notes in the vault.
How It Works
1. Find all active prep notes:
grep -rl "type: $TYPE_TAG" $MEETINGS_DIR 2>/dev/nullFilter to only notes where status: prep (skip done, cancelled, orphaned).
2. Extract booking identifiers from each prep note's frontmatter:
date(YYYY-MM-DD)participant(full name)email(attendee email)event_type(Cal.com event type name)
3. Check Cal.com state: Load Cal.com MCP tools via ToolSearch (query: "calcom", max_results: 20). Call get_bookings to retrieve bookings covering the date range of all active prep notes. For each prep note, match against Cal.com bookings by date + attendee email.
4. Update based on booking status:
Cancelled — the booking exists on Cal.com with a cancelled status:
- Update frontmatter:
status: prep→status: cancelled - Append a section to the note:
## Cancelled
Booking was cancelled on YYYY-MM-DD (detected during sync).Rescheduled — the booking exists but with a different date/time:
- Update the
datefield in frontmatter to the new date - Update the
timefield to the new time - Rename the file from
YYYYMMDD-$PREP_PREFIX-participant-slug.mdtoNEW_YYYYMMDD-$PREP_PREFIX-participant-slug.md - Append a section to the note:
## Rescheduled
Moved from ORIGINAL_DATE to NEW_DATE (detected during sync on YYYY-MM-DD).Still active — booking exists, same date/time, accepted status:
- No changes needed.
5. Report what changed: Output a summary table listing each prep note and the action taken (no change / cancelled / rescheduled / orphaned).
Edge Cases
- Booking not found on Cal.com (may have been deleted entirely, or is older than Cal.com's retention): Update frontmatter
status: prep→status: orphaned. Append:
## Orphaned
No matching booking found on Cal.com (detected during sync on YYYY-MM-DD). The booking may have been deleted.- Multiple bookings on the same day with the same participant: Match by
event_typein addition to date + attendee email to disambiguate. If ambiguity remains, skip the note and flag it in the report for manual review. - Prep notes with `status: done`: Skip entirely — these are already linked to session notes and should not be modified by sync.
Status & Queries: meeting-prep status
When invoked with status, scan all prep notes and report their current state. Also output useful Dataview queries the user can paste into their vault.
Status Report
Scan $MEETINGS_DIR for files matching *-$PREP_PREFIX-*.md. For each, read frontmatter and report:
| Date | Participant | Status | Session Note |
|---|---|---|---|
from date | from participant | prep or done | linked note or — |
After the table, output a Top 5 most-met participants summary by scanning all prep notes in the vault, counting occurrences per participant, and listing the top 5 with their session count and date of last meeting.
Dataview Queries
Include these ready-to-paste queries in the status output:
Upcoming preps (unlinked): ````markdown
TABLE participant AS "Who", time AS "Time", event_type AS "Type", duration + " min" AS "Duration"
FROM "Meetings"
WHERE type = "$TYPE_TAG" AND status = "prep"
SORT date ASC````
Completed preps (linked to session notes): ````markdown
TABLE participant AS "Who", date AS "Date", session_note AS "Session Note"
FROM "Meetings"
WHERE type = "$TYPE_TAG" AND status = "done"
SORT date DESC````
Prep coverage — sessions without a prep note: ````markdown
TABLE title AS "Session", date AS "Date", participants AS "Participants"
FROM "Meetings"
WHERE !contains(file.name, "$PREP_PREFIX") AND !contains(file.inlinks.file.name, "$PREP_PREFIX")
SORT date DESC
LIMIT 20````
All preps by participant: ````markdown
TABLE date AS "Date", status AS "Status", event_type AS "Type", session_note AS "Linked"
FROM "Meetings"
WHERE type = "$TYPE_TAG"
SORT participant ASC, date DESC````
Relationship & Activity Queries
Meeting frequency by participant: ````markdown
TABLE length(rows) AS "Sessions", min(rows.date) AS "First", max(rows.date) AS "Last"
FROM "Meetings"
WHERE type = "$TYPE_TAG"
GROUP BY participant
SORT length(rows) DESC````
Participant timeline (embed in a participant's person note — uses this.participant for context): ````markdown
TABLE date AS "Date", event_type AS "Type", status AS "Status", session_note AS "Linked"
FROM "Meetings"
WHERE type = "$TYPE_TAG" AND participant = this.participant
SORT date DESC````
Recent meeting activity (last 30 days, all sessions, cross-referenced with prep coverage): ````markdown
TABLE title AS "Session", participants AS "With",
choice(contains(file.inlinks.file.name, "prep"), "✓", "—") AS "Prepped"
FROM "Meetings"
WHERE date >= date(today) - dur(30 days) AND !contains(file.name, "prep")
SORT date DESC````
First-timers list (people met only once — potential follow-up candidates): ````markdown
TABLE participant AS "Who", date AS "Met On", event_type AS "Type"
FROM "Meetings"
WHERE type = "$TYPE_TAG"
GROUP BY participant
FLATTEN length(rows) AS count
WHERE count = 1
FLATTEN rows.date AS date
FLATTEN rows.event_type AS event_type
SORT date DESC````
# meeting-prep skill configuration
# All paths support ~ (home directory expansion)
vault:
root: ~/Brains/brain
meetings: ~/Brains/brain/Meetings
# Where to search for person/contact notes (outside Meetings/)
people_search_depth: 2
prep_notes:
# Filename pattern: {date}-prep-{participant_slug}.md
prefix: prep
# Frontmatter fields added to every prep note
type_tag: meeting-prep
# Host identity — used in participant research prompts
host:
name: Gleb Kalinin
role: AI lab instructor and consultant
location: Berlin
# Cal.com settings
calcom:
# Default lookahead for bookings when no date specified
lookahead_days: 7
timezone: Europe/Berlin
time_format: 24h
export const meta = {
name: 'meeting-prep-research',
description: 'Research meeting participants in parallel and compile prep notes',
phases: [
{ title: 'Research', detail: 'Web research + vault history per participant' },
{ title: 'Verify', detail: 'Cross-check research claims for accuracy' },
{ title: 'Compile', detail: 'Create prep notes in Obsidian vault' }
]
}
const RESEARCH_SCHEMA = {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string' },
role: { type: 'string', description: 'Current role and company' },
company_context: { type: 'string', description: 'What the company does, size, recent news' },
linkedin_summary: { type: 'string', description: 'Career highlights, expertise areas' },
mutual_context: { type: 'string', description: 'Shared connections, projects, or context with the meeting host' },
conversation_starters: {
type: 'array',
items: { type: 'string' },
description: '2-3 relevant conversation starters'
},
session_count: { type: 'number', description: 'Number of previous sessions found in vault' },
previous_sessions: {
type: 'array',
items: {
type: 'object',
properties: {
date: { type: 'string' },
title: { type: 'string' },
duration: { type: 'string' },
key_topics: { type: 'array', items: { type: 'string' } },
action_items: { type: 'array', items: { type: 'string' } },
source: { type: 'string' }
}
}
},
recurring_themes: { type: 'array', items: { type: 'string' } },
outstanding_followups: { type: 'array', items: { type: 'string' } }
},
required: ['name', 'role', 'session_count']
}
const VERIFY_SCHEMA = {
type: 'object',
properties: {
name: { type: 'string' },
email_domain_match: { type: 'boolean', description: 'Does the claimed company match the email domain?' },
confidence: { type: 'string', enum: ['high', 'medium', 'low'], description: 'Overall confidence in research accuracy' },
flags: {
type: 'array',
items: { type: 'string' },
description: 'Suspicious or unverifiable claims'
},
verified_role: { type: 'string', description: 'Verified or best-guess role' },
verified_company: { type: 'string', description: 'Verified or best-guess company' }
},
required: ['name', 'email_domain_match', 'confidence', 'flags', 'verified_role', 'verified_company']
}
// Parse args — comes as string from Workflow tool
const parsedArgs = typeof args === 'string' ? JSON.parse(args) : (args || {})
const bookings = parsedArgs.bookings || []
const config = parsedArgs.config || {}
// Config-driven paths (passed by the skill, read from config.yaml)
if (!config.meetings_dir || !config.vault_root) {
throw new Error('config.meetings_dir and config.vault_root are required — pass them from config.yaml via workflow args')
}
const MEETINGS_DIR = config.meetings_dir
const VAULT_ROOT = config.vault_root
const PEOPLE_DEPTH = config.people_search_depth || 2
const PREP_PREFIX = config.prep_prefix || 'prep'
const TYPE_TAG = config.type_tag || 'meeting-prep'
const TIMEZONE = config.timezone || 'UTC'
const HOST_NAME = config.host_name || 'the meeting host'
const HOST_ROLE = config.host_role || ''
const HOST_LOCATION = config.host_location || ''
// Normalize attendees: each booking may have a single participant (name+email)
// or an attendees array [{name, email, timezone}, ...] for group sessions.
for (const b of bookings) {
if (!b.attendees || b.attendees.length === 0) {
b.attendees = [{ name: b.participant, email: b.email, timezone: b.timezone }]
}
}
const uniqueParticipants = []
const seenEmails = new Set()
for (const b of bookings) {
for (const att of b.attendees) {
if (!seenEmails.has(att.email)) {
seenEmails.add(att.email)
uniqueParticipants.push({ name: att.name, email: att.email })
}
}
}
log('Preparing ' + bookings.length + ' sessions for ' + uniqueParticipants.length + ' unique participants')
phase('Research')
const researchAndVerify = await parallel(
uniqueParticipants.map((p) => () =>
pipeline([
() => agent(
'Research this person AND search the Obsidian vault for previous session history.\n\n' +
'PERSON:\n' +
'Name: ' + p.name + '\n' +
'Email: ' + p.email + '\n' +
'Email domain: ' + p.email.split('@')[1] + '\n\n' +
'PART 1 — WEB RESEARCH:\n' +
'Search the web for their name + company/domain. Check LinkedIn.' + (HOST_LOCATION ? ' Look for connections to the ' + HOST_LOCATION + ' tech scene.' : '') + ' The meeting host is ' + HOST_NAME + (HOST_ROLE ? ', ' + HOST_ROLE : '') + (HOST_LOCATION ? ' in ' + HOST_LOCATION : '') + '.\n\n' +
'PART 2 — VAULT HISTORY:\n' +
'Search for previous meeting notes in ' + MEETINGS_DIR + '\n' +
'1. Run: grep -rl "' + p.name + '" ' + MEETINGS_DIR + ' 2>/dev/null\n' +
'2. Also try first name only: grep -rl "' + p.name.split(' ')[0] + '" ' + MEETINGS_DIR + ' 2>/dev/null\n' +
'3. Read matching files (up to 5 most recent by date prefix) and extract: date, duration, key topics, action items, source (fathom/granola/manual)\n' +
'4. Also check: find ' + VAULT_ROOT + ' -maxdepth ' + PEOPLE_DEPTH + ' -name "*' + (p.name.split(' ')[1] || p.name.split(' ')[0]) + '*" -not -path "*/Meetings/*" 2>/dev/null\n\n' +
'Return everything in the structured schema. If no previous sessions found, set session_count to 0. Do not fabricate information.',
{ label: 'profile:' + p.name, phase: 'Research', schema: RESEARCH_SCHEMA }
),
(profile) => {
if (!profile || !profile.role) return { profile, verification: null }
const domain = p.email.split('@')[1]
return agent(
'Verify the accuracy of this research profile. Cross-check claims against the email domain and look for red flags.\n\n' +
'PERSON: ' + p.name + '\n' +
'EMAIL DOMAIN: ' + domain + '\n\n' +
'RESEARCH PROFILE:\n' + JSON.stringify(profile, null, 2) + '\n\n' +
'CHECK THE FOLLOWING:\n' +
'1. Does the claimed company match the email domain "' + domain + '"? For example, if the domain is "all3.com" the company should be All3 or a known subsidiary/brand. Set email_domain_match accordingly.\n' +
'2. Are there claims that seem implausible or could be confused with a different person of the same name? List each suspicious claim in "flags".\n' +
'3. Assign a confidence level (high/medium/low) for the overall profile. Use "low" if the company doesn\'t match the domain or multiple claims look wrong. Use "medium" if there are minor uncertainties. Use "high" if everything checks out.\n' +
'4. Provide verified_role and verified_company with your best understanding of their actual role and company.\n\n' +
'Return the structured verification result.',
{ label: 'verify:' + p.name, phase: 'Verify', schema: VERIFY_SCHEMA }
).then((v) => ({ profile, verification: v }))
}
])
)
)
phase('Verify')
const profileByEmail = {}
for (let i = 0; i < uniqueParticipants.length; i++) {
const result = researchAndVerify[i]
if (result && result.profile) {
const profile = result.profile
const v = result.verification
if (v) {
profile.research_confidence = v.confidence || 'medium'
profile.research_flags = v.flags || []
if (v.verified_role) profile.role = v.verified_role
if (v.verified_company) profile.company_context = v.verified_company + (profile.company_context ? ' — ' + profile.company_context : '')
} else {
profile.research_confidence = 'medium'
profile.research_flags = ['Verification skipped — insufficient research data']
}
profileByEmail[uniqueParticipants[i].email] = profile
}
}
log('Research & verification complete. Creating ' + bookings.length + ' prep notes.')
phase('Compile')
// Sort bookings by date so earlier sessions are processed first
const sortedBookings = [...bookings].sort((a, b) => a.date.localeCompare(b.date))
// Track prior prep notes per participant (by email) for sequential booking handling
const priorPrepByEmail = {}
const prepResults = await parallel(
sortedBookings.map((booking) => () => {
const isGroup = booking.attendees.length > 1
const dateSlug = booking.date.replace(/-/g, '')
if (isGroup) {
// --- Group session: one note for all attendees ---
const eventSlug = (booking.event_type_slug || booking.event_type).toLowerCase().replace(/\s+/g, '-')
const filePath = MEETINGS_DIR + '/' + dateSlug + '-' + PREP_PREFIX + '-' + eventSlug + '.md'
const attendeeProfiles = booking.attendees.map((att) => ({
...att,
profile: profileByEmail[att.email] || {}
}))
const attendeeYaml = booking.attendees.map((att) =>
' - name: "' + att.name + '"\n email: ' + att.email
).join('\n')
const attendeeSections = attendeeProfiles.map((att) => {
const p = att.profile
const hasHistory = p.session_count && p.session_count > 0
return '### ' + att.name + '\n' +
'Summarize research: role, company, relevant context.\n' +
'PROFILE: ' + JSON.stringify(p, null, 2) + '\n' +
(hasHistory
? 'Previous sessions: ' + p.session_count + '. Summarize key topics and continuity.\n'
: 'First-time participant — no vault history.\n')
}).join('\n')
return agent(
'Create a GROUP meeting prep note as a markdown file.\n\n' +
'WRITE THIS FILE: ' + filePath + '\n\n' +
'BOOKING:\n' +
'- Event type: ' + booking.event_type + '\n' +
'- Date: ' + booking.date + '\n' +
'- Time: ' + booking.time_cet + ' ' + TIMEZONE + '\n' +
'- Duration: ' + booking.duration + ' min\n' +
'- Zoom: ' + (booking.zoom_link || 'TBD') + '\n' +
'- Attendees: ' + booking.attendees.length + '\n\n' +
'ALL ATTENDEE PROFILES:\n' + JSON.stringify(attendeeProfiles.map((a) => ({ name: a.name, email: a.email, profile: a.profile })), null, 2) + '\n\n' +
'TEMPLATE:\n' +
'---\n' +
'type: ' + TYPE_TAG + '\n' +
'date: ' + booking.date + '\n' +
'participants:\n' + attendeeYaml + '\n' +
'event_type: "' + booking.event_type + '"\n' +
'duration: ' + booking.duration + '\n' +
'time: "' + booking.time_cet + '"\n' +
'status: prep\n' +
'session_note: ""\n' +
'tags:\n - ' + TYPE_TAG + '\n' +
'---\n\n' +
'# Prep: ' + booking.event_type + ' — ' + booking.date + '\n\n' +
'## Session Info\n' +
'Fill in time, type, location. Use ' + TIMEZONE + ' for display.\n' +
'Attendees: ' + booking.attendees.length + ' participants.\n\n' +
'## Attendees\n\n' +
attendeeSections + '\n' +
'## Previous Sessions per Attendee\n' +
'Summarize vault history grouped by person. Skip attendees with no history.\n\n' +
'## Prep Ideas\n' +
'- Group dynamics to consider\n' +
'- Topics to cover with the full group\n' +
'- Individual follow-ups to weave in\n\n' +
'## Notes\n' +
'_Space for pre-meeting thoughts_\n\n' +
'IMPORTANT: Write the actual file using the Write tool. Fill in real content from the profile data, do not leave placeholders. Return the file path when done.',
{ label: 'note:' + booking.event_type + ' (' + booking.date + ', ' + booking.attendees.length + ' attendees)', phase: 'Compile' }
)
} else {
// --- Solo session: one note per participant ---
const profile = profileByEmail[booking.attendees[0].email] || {}
const participant = booking.attendees[0].name || booking.participant
const nameSlug = participant.toLowerCase().replace(/\s+/g, '-')
const filePath = MEETINGS_DIR + '/' + dateSlug + '-' + PREP_PREFIX + '-' + nameSlug + '.md'
const hasHistory = profile.session_count && profile.session_count > 0
const lowConfidence = profile.research_confidence === 'low'
const researchFlags = profile.research_flags || []
// Check if this participant already has an earlier prep note in this batch
const prior = priorPrepByEmail[booking.attendees[0].email]
// Record this booking as the latest prep for this participant
priorPrepByEmail[booking.attendees[0].email] = { path: filePath, date: booking.date, slug: dateSlug + '-' + PREP_PREFIX + '-' + nameSlug }
const priorPrepInstruction = prior
? '\nSEQUENTIAL BOOKING — This participant has an earlier session on ' + prior.date + '.\n' +
'After ## Session Info, add a ## Prior Session section:\n' +
' "A session with ' + participant.split(' ')[0] + ' is scheduled for ' + prior.date + ' — it will have happened by the time this session occurs.\n' +
' Update this note after the ' + prior.date + ' session with fresh context."\n' +
'Keep all research/history sections but add a note: "(will be stale after ' + prior.date + ')"\n\n'
: ''
return agent(
'Create a meeting prep note as a markdown file.\n\n' +
'WRITE THIS FILE: ' + filePath + '\n\n' +
priorPrepInstruction +
'BOOKING:\n' +
'- Participant: ' + participant + '\n' +
'- Email: ' + booking.attendees[0].email + '\n' +
'- Date: ' + booking.date + '\n' +
'- Time: ' + booking.time_cet + ' ' + TIMEZONE + '\n' +
'- Duration: ' + booking.duration + ' min\n' +
'- Event type: ' + booking.event_type + '\n' +
'- Zoom: ' + (booking.zoom_link || 'TBD') + '\n' +
'- Participant timezone: ' + (booking.attendees[0].timezone || booking.timezone) + '\n\n' +
'PROFILE & RESEARCH:\n' + JSON.stringify(profile, null, 2) + '\n\n' +
'TEMPLATE:\n' +
'---\n' +
'type: ' + TYPE_TAG + '\n' +
'date: ' + booking.date + '\n' +
'participant: "' + participant + '"\n' +
'email: ' + booking.attendees[0].email + '\n' +
'event_type: "' + booking.event_type + '"\n' +
'duration: ' + booking.duration + '\n' +
'time: "' + booking.time_cet + '"\n' +
(prior ? 'prior_prep: "[[' + prior.slug + ']]"\n' : '') +
'status: prep\n' +
'session_note: ""\n' +
'tags:\n - ' + TYPE_TAG + '\n' +
'---\n\n' +
'# Prep: ' + participant + ' — ' + booking.date + '\n\n' +
(lowConfidence
? '> [!warning]\n> Research confidence is low — verify key facts before the meeting.\n' +
(researchFlags.length > 0 ? '> Flags: ' + researchFlags.join('; ') + '\n' : '') + '\n'
: (researchFlags.length > 0
? '> [!note] Research flags\n> ' + researchFlags.join('; ') + '\n\n'
: '')) +
'## Session Info\n' +
'Fill in time, type, location, timezone. Use ' + TIMEZONE + ' for display.\n\n' +
(prior
? '## Prior Session\n' +
'A session with ' + participant.split(' ')[0] + ' is scheduled for ' + prior.date + ' — it will have happened by the time this session occurs.\n' +
'Update this note after the ' + prior.date + ' session with fresh context.\n\n'
: '') +
'## About ' + participant.split(' ')[0] + '\n' +
'Summarize research: role, company, relevant context. Keep concise.\n\n' +
(hasHistory
? '## Previous Sessions (' + profile.session_count + ' total)' + (prior ? ' _(will be stale after ' + prior.date + ')_' : '') + '\n' +
'Summarize the most recent sessions with dates, topics, outcomes.\n' +
'Use data from profile.previous_sessions.\n\n' +
'## Continuity & Follow-ups' + (prior ? ' _(will be stale after ' + prior.date + ')_' : '') + '\n' +
'List outstanding action items, recurring themes, progress.\n' +
'Use profile.outstanding_followups and profile.recurring_themes.\n\n'
: '## First Meeting\nNo previous sessions found in vault.\n\n' +
'## Context & Talking Points\n' +
'Research-based starters, questions to understand their goals.\n\n') +
'## Prep Ideas\n' +
'- Conversation starters (use profile.conversation_starters)\n' +
'- Topics to explore\n' +
'- Questions to ask\n\n' +
'## Notes\n' +
'_Space for pre-meeting thoughts_\n\n' +
'IMPORTANT: Write the actual file using the Write tool. Fill in real content from the profile data, do not leave placeholders. Return the file path when done.',
{ label: 'note:' + participant + ' (' + booking.date + ')', phase: 'Compile' }
)
}
})
)
return {
bookings_prepped: bookings.length,
participants_researched: uniqueParticipants.length,
prep_notes: prepResults.filter(Boolean)
}
{
"name": "meeting-prep",
"version": "1.0.0",
"description": "Prepare for upcoming meetings — pulls Cal.com bookings, researches participants, audits previous sessions from Obsidian vault, and creates prep notes. Also links prep notes to post-meeting session notes.",
"trigger": {
"keywords": ["meeting prep", "prep for meeting", "upcoming meetings", "session prep", "who do I have", "prepare for calls", "meeting-prep"],
"patterns": ["prep for", "prepare for my", "who.*coming up", "meeting prep"]
},
"capabilities": [
"Fetches upcoming Cal.com bookings and prepares comprehensive prep notes in Obsidian",
"Researches participants via web search and cross-references vault session history",
"Links prep notes to post-meeting session notes (link mode)",
"Syncs prep notes with Cal.com booking state — detects cancellations, reschedules, deletions (sync mode)",
"Reports status of all prep notes with ready-to-paste Dataview queries (status mode)",
"Supports solo and group session templates with per-attendee research"
]
}