| name | process-meetings |
| description | Turn synced meetings into updated person pages, extracted tasks and organized notes. Use when the user says 'process my meetings', 'catch up my notes', or after Granola/Otter syncs. Also use proactively when unprocessed meetings exist. Not for prepping an upcoming meeting; use `meeting-prep`. |
| model_hint | balanced |
Process Meetings
Process meetings that have been synced from Granola by the background automation. Updates person pages, extracts tasks, and organizes meeting notes.
Background Execution
This skill supports background execution. When invoked:
- Acknowledge: "Processing [N] meetings in the background. I'll let you know when done."
- Process all meetings
- On completion, provide summary: "[N] meetings processed. [X] person pages updated. [Y] action items created."
How It Works
Meetings are synced automatically every 30 minutes by a background process. This command reads those synced files and:
- Creates/updates person and company pages
- Extracts action items to 03-Tasks/Tasks.md
- Links everything together
No terminal commands are shown - the heavy lifting happens in the background.
Arguments
- No arguments: Process all unprocessed meetings from the last 7 days
today: Only process today's meetings
"search term": Find meetings by title/attendee
--people-only: Only update person/company pages (skip tasks)
--no-todos: Create notes but don't extract tasks
--setup: Install/check background automation
Pre-flight: Local Source Check
Read meeting_sources in System/user-profile.yaml before assuming a recorder.
The configured primary is provenance, not permission or tool access. A valid
vault-relative notes_folder outranks the default landing zone. Missing or
malformed config, an invalid primary, an absolute path, .., the vault root, or
a symlink escape must be reported and ignored; continue with safe local notes.
Never widen into an external service just because the profile names it.
For primary: granola, Granola sync uses the official public API. If
GRANOLA_API_KEY is absent, offer /granola-setup and continue with local
notes. Other primaries do not inherit a direct reader from this setting.
Process
Step 1: Check Source and Background Sync Status
Resolve meeting_sources.notes_folder using the rules above: only a valid
vault-relative folder is accepted; missing or malformed config, an absolute
path, .., the vault root, or a symlink escape falls back safely. The configured
primary does not grant access to an external service. Then check whether
Granola background sync has left its optional state file:
ls .scripts/meeting-intel/processed-meetings.json
If the state file exists: Granola background sync has run. Continue to Step 2.
If it does not exist and Granola is configured: offer setup, but continue
with local notes. The missing file is not a gate for an exported-folder, Zoom,
Teams, manual note, or provider-neutral local source.
For Granola setup guidance:
"Background meeting sync isn't set up yet. This runs automatically every 30 minutes so /process-meetings doesn't need terminal commands.
To set up (one-time, takes 30 seconds):
cd .scripts/meeting-intel && ./install-automation.sh
Or run /process-meetings --setup and I'll do it for you.
Requirements:
- A Granola Business plan, with your Granola API key connected via
/granola-setup
- An LLM API key in
.env (GEMINI_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY)"
If user runs --setup:
cd .scripts/meeting-intel && ./install-automation.sh
Step 2: Find Synced Meetings
Read the processed meetings state when it exists:
const state = JSON.parse(fs.readFileSync('.scripts/meeting-intel/processed-meetings.json'));
Search the valid configured folder first, then 00-Inbox/Meetings/. If neither
contains a candidate, use bounded provider-neutral Markdown discovery in the
vault by date plus title, attendee, or meeting frontmatter. Exclude Dex
internals, dependencies, binaries, and archives outside the requested window;
never treat arbitrary Markdown as a meeting. For the default folder:
find 00-Inbox/Meetings -name "*.md" -mtime -7 | head -50
For each meeting file, including a manual note with no capture id:
- Preserve its actual vault-relative path. Read
participants, company,
date, and recorder provenance. A capture id is a non-empty scalar key
ending in _id (for example granola_id or wispr_id). Prefer the key
matching a string source; if that key is absent, report the mismatch and
use the note path. When source is absent, use an id only when exactly one
non-empty scalar candidate exists. Empty or non-scalar values do not count;
multiple ids fall back to note-path identity. The path identity is the
normalized vault-relative Markdown path, with / separators and the .md
extension retained; never reduce it to a basename
- Check if person/company pages need updating
- Check if tasks need extracting (look for unchecked items in "For Me" section)
Report findings:
"Found X synced meetings from the last 7 days. Y need person page updates, Z have unextracted tasks."
Match capture identity to Calendar
For a synced note with an aware ISO capture_started_at, call
calendar_get_events_with_attendees for that date after applying CLAUDE.md's
Calendar response confidence contract, then call the Work MCP
match_capture_to_calendar tool with the capture title, start time, attendees,
and the Calendar response's events array as calendar_events (not the whole
response object). Use a matched result's identity only (title, normalized
start, attendees); if the safe title differs, carry it into the note. Leave an
unmatched or ambiguous capture unchanged, and continue unchanged when Calendar
is unavailable. The matcher owns the hard five-minute limit, timezone parsing,
tie order, poor-title rule, and ambiguity decision—never redo or stretch them.
Never copy join URLs, dial-ins, access codes, location, notes, descriptions,
conferencing fields, or any other invite payload.
Step 3: Update Person Pages
For each participant in synced meetings:
-
Load user profile for email domain:
Read System/user-profile.yaml → get email_domain
-
Classify as Internal/External:
- If participant email domain matches user's domain → Internal
- Otherwise → External
-
Look up the person with the Work MCP lookup_person tool.
- If lookup returns
ambiguous: true, do not create a page. Surface the possible matches to the user.
- If a match exists, update that existing page.
-
If no match exists, call the Work MCP create_person tool:
- Pass
name, role when known, emails from the meeting's attendees block, and location from that attendee's location field.
- Pass the meeting company and a short source note when available.
---
type: person
name: "{Name}"
role: null
company: "{company from meeting}"
company_page: null
emails: ["{lowercased email, if available}"]
aliases: []
location: {internal|external}
last_interaction: {meeting date}
---
# {Name}
## Notes
*Auto-created from meeting on {date}*
## Recent Interactions
<!-- dex:auto:recent-interactions -->
- [{Meeting Title}](00-Inbox/Meetings/{date}/{slug}.md) — {date}
<!-- /dex:auto -->
## Key Context
```
5. **If page exists, add meeting to Recent Interactions:**
- Read existing page
- Add new meeting link under "## Recent Interactions"
- Keep max 20 entries (remove oldest if needed)
- Update "Last Interaction" in frontmatter
### Step 4: Update Company Pages
For each unique external company domain:
1. **Check if company page exists:** `05-Areas/Companies/{Company}.md`
2. **If doesn't exist, create it:**
```markdown
---
type: company
name: "{Company Name}"
domains: ["{lowercased domain}"]
website: "{website, if known}"
status: "Prospect"
---
# {Company Name}
## Key Contacts
<!-- dex:auto:key-contacts -->
- [[05-Areas/People/External/{Person}|{Person}]]
<!-- /dex:auto -->
## Meeting History
<!-- dex:auto:meeting-history -->
- [{Meeting Title}](00-Inbox/Meetings/{date}/{slug}.md) — {date}
<!-- /dex:auto -->
## Notes
*Auto-created from meeting on {date}*
- If exists, update:
- Add any new contacts to "Key Contacts"
- Add meeting to "Meeting History"
Step 4.5: Semantic Enrichment (if QMD available)
Check if semantic search is available by looking for qmd in PATH.
If available, enhance meeting processing with meaning-based intelligence:
-
Detect implicit commitments: For each meeting's discussion notes, search semantically:
qmd query "we should circle back on..." --limit 3
qmd query "let me think about..." --limit 3
Catch soft commitments that regex action-item extraction misses.
- Examples: "we should probably revisit the pricing model" → implicit action item
- "I need to noodle on the migration approach" → implicit commitment
- "Let's reconnect after the board meeting" → implicit follow-up
-
Link meetings to projects: For the meeting topic, search:
qmd query "meeting topic/title" --limit 3
against 04-Projects/ to auto-link the meeting to relevant projects that keyword matching would miss.
-
Enrich person context: For each new person encountered, search:
qmd query "person name + company" --limit 3
Find if they've been mentioned in other meetings/notes, even if they weren't a direct participant.
Deterministic soft-commitment pass (always runs): Independently of QMD
availability, run the detect_soft_commitments Work-MCP tool over each meeting's
discussion notes. Add matches to the action-items list marked
"(soft commitment — confirm before creating)" so Step 5 confirms, creates, and
reads back every task ID. QMD is the semantic complement; NEVER auto-create.
Integration:
- Add implicit commitments to the action items list with a note: "(detected — not explicitly stated)"
- Add project links to meeting frontmatter
- Merge person context into newly-created person pages
- If QMD unavailable, skip silently — regex extraction still works
Step 5: Extract Tasks (unless --no-todos or --people-only)
For each meeting with unextracted tasks:
-
Find action items in the "## Action Items > ### For Me" section
-
For each unchecked item (- [ ]):
- Extract task description
- Read pillar from meeting frontmatter, then resolve it to the unique pillar
ID in
System/pillars.yaml by matching either id or display name
- Preserve the exact source checkbox line text for
stamp_source_line
- Let
create_task generate the task ID and stamp it back onto that line
-
Create task using Work MCP:
create_task(
title: "Task description",
priority: "P2", // default, P1 if "urgent" mentioned
pillar: "{resolved pillar ID}",
people: ["{participant page paths}"],
source: "{meeting path}",
stamp_source_line: "{exact source checkbox line text}"
)
people values must resolve to existing person page paths. Prefer the paths
returned by Step 3's lookup_person/create_person flow; if only a bare
participant name is available, pass that name unchanged and let create_task
resolve it. Never construct or guess a person page path.
-
Verify every result before marking the meeting extracted:
- Require
success: true for every create_task call.
- Require either
stamp.stamped: true, or reason: "already_anchored"
with the exact source line's existing anchor equal to the returned
task.task_id.
- If entity resolution or stamping is unresolved, surface the exact failed
line and leave the meeting unmarked for reconciliation. Do not blindly
retry a task that was created but not stamped.
Only after every action item is verified, add this comment to the meeting note:
<!-- tasks-extracted: 2026-02-03T10:30:00Z -->
Step 6: Auto-link People in Processed Notes
After finishing edits to each processed meeting note, run this once for every processed note:
node .scripts/auto-link-people.cjs "<note-file>"
Use node .scripts/auto-link-people.cjs --dry-run "<note-file>" to preview what would be linked without changing the file.
Step 7: Verify Entity Coverage
Run node .scripts/meeting-intel/verify-entities.cjs and show its one-line summary.
If ENTITY_SUGGESTIONS_FILE contains suggested people, list them and ask: "Want me to create these pages? (creates via create_person; dismiss or never also fine)"
- Accepted: call
create_person, set the suggestion to accepted, and set the contact state to created with its page path.
- Dismissed: set the suggestion to
dismissed.
- Never: set the suggestion to
suppressed.
Step 8: Summary Report
## Meeting Processing Complete ✅
**Synced meetings found:** X (last 7 days)
**Background sync status:** Running (last sync: 10 min ago)
### Updates Made
**Person pages:**
- Created: 3 new (Alice Chen, Bob Smith, Carol Wang)
- Updated: 5 existing
**Company pages:**
- Created: 1 new (Acme Corp)
- Updated: 2 existing
**Tasks extracted:** 7 items added to 03-Tasks/Tasks.md
### Recent Meetings
| Date | Meeting | Company | Participants |
|------|---------|---------|--------------|
| Feb 3 | Product Review | Acme | Alice, Bob |
| Feb 2 | Strategy Call | BigCo | Carol |
---
*Background sync runs every 30 min. Check status: `.scripts/meeting-intel/install-automation.sh --status`*
Error Handling
For MCP responses, follow CLAUDE.md's feature_status rendering convention before applying these fallbacks.
If no meetings found:
"No meetings synced in the last 7 days. Make sure:
- Your Granola API key is connected (run
/granola-setup if not)
- Background sync is set up (run
/process-meetings --setup)
- Check logs:
.scripts/logs/meeting-intel.stdout.log"
If background sync isn't running:
"Background sync appears to be stopped. To restart:
cd .scripts/meeting-intel && ./install-automation.sh
```"
Examples
/process-meetings
"Found 8 synced meetings. Updating 12 person pages, extracting 5 tasks..."
/process-meetings today
"Found 2 meetings from today. Processing..."
/process-meetings --setup
"Installing background automation..." [runs install script]
/process-meetings --people-only
"Updating person and company pages only (skipping task extraction)..."
Track Usage (Silent)
Update System/usage_log.md to mark meeting processing as used.
Analytics (Silent):
Call track_event with event_name meeting_processed and properties:
meetings_count: number of meetings processed
people_created: number of new person pages created
todos_extracted: number of tasks extracted
This only fires if the user has opted into analytics. No action needed if it returns "analytics_disabled".