Building a Meeting Intelligence System with Claude and Slack

I take a lot of meetings. Between running multiple companies and advising others, my calendar is packed. Granola has been fantastic for recording and transcribing them, but I kept running into the same problem: action items would sit in transcripts that I'd never revisit.

Things slipped through the cracks. I'd remember a commitment I made three days later, scrambling to follow up. Or worse, I'd completely forget until someone pinged me asking for a status update.

So I built granola-tools - an AI-powered meeting assistant that extracts action items from my Granola transcripts and posts them to Slack with approve/dismiss buttons. One click creates a Linear ticket. Another drafts a follow-up email.

Here's how it works.


The Architecture

The system has three phases:

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Granola   │────▶│  Processor   │────▶│    Slack    │
│   (cache)   │     │  (Claude AI) │     │  (channel)  │
└─────────────┘     └──────────────┘     └──────┬──────┘
                                               │
                   ┌──────────────┐            │ button click
                   │   Webhook    │◀───────────┘
                   │   Server     │
                   └──────┬───────┘
                          │
             ┌────────────┴────────────┐
             ▼                         ▼
       ┌──────────┐             ┌──────────┐
       │  Linear  │             │  Gmail   │
       │ (ticket) │             │ (draft)  │
       └──────────┘             └──────────┘

Phase 1: Sync

The sync agent runs on a schedule (via launchd locally, GitHub Actions in production). It:

  1. Fetches new meetings from Granola's local cache
  2. Uses Claude to categorize each meeting by context (which client? which project?)
  3. Generates vector embeddings for semantic search later
  4. Stores everything in Supabase

The categorization is important because different contexts have different rules. A Cheerful client call should create tickets in the Cheerful Linear team. A personal 1:1 might just need a follow-up email.

Phase 2: Propose

For each new meeting, the workflow proposer analyzes the transcript with Claude. The prompt includes:

  • The full transcript
  • Context-specific rules (from workflow-rules.md)
  • The meeting category
  • Recent related meetings (for context)

Claude extracts action items and proposes specific actions:

class ActionType(Enum):
    LINEAR_CREATE = "linear_create"
    LINEAR_UPDATE = "linear_update"
    EMAIL_DRAFT = "email_draft"
    CALENDAR_CREATE = "calendar_create"

Each proposal includes structured data: title, description, owner, priority, due date (if mentioned).

Phase 3: Execute

Proposals get posted to a Slack channel as interactive messages:

📅 Weekly Team Sync
December 29, 2025

Summary: Discussed Q1 roadmap and resource allocation...

✅ Action Items (3)
🔴 Finalize budget proposal (Owner: Chris)
🟡 Schedule design review
🟢 Update documentation

[Create Ticket] [Create All Tickets] [Generate Email] [Dismiss]

When I click a button, a webhook server receives the interaction and executes the action:

  • Create Ticket: Calls Linear's GraphQL API to create an issue with the right team, labels, and assignee
  • Generate Email: Uses Claude to draft a follow-up email based on the meeting context
  • Dismiss: Marks the proposal as reviewed, won't show again

The Context System

Not all meetings are equal. A client call needs different handling than an internal sync. The context system manages this with a workflow-rules.md file:

## cheerful

**Linear Team**: Cheerful
**Gmail Account**: chris@cheerful.ai
**Calendar**: chris@cheerful.ai

**Allowed Actions**:
- LINEAR_CREATE
- LINEAR_UPDATE
- EMAIL_DRAFT

**Rules**:
- Always include client name in ticket titles
- Default priority is P2 unless urgency is mentioned
- Tag tickets with "client-request" label
- Follow-up emails should be professional but warm

## personal

**Linear Team**: Personal
**Gmail Account**: chris@brownridges.com

**Allowed Actions**:
- LINEAR_CREATE
- EMAIL_DRAFT
- CALENDAR_CREATE

**Rules**:
- Keep tickets lightweight
- Calendar events for anything with a specific time mentioned

The sync agent uses Claude to classify each meeting into a context based on participants, meeting title, and content. Then the proposer applies the right rules.


The Slack Assistant

Beyond action item extraction, I added a Q&A mode. In a private Slack channel, I can ask questions about past meetings:

Me: What did Sarah say about the timeline in our last Acme call?

Bot: Based on your call with Acme Corp on December 15:
     Sarah mentioned the timeline twice:
     1. "We're targeting end of Q1 for the initial rollout"
     2. "The board wants to see results by March"

     Sources:
     • Intro Call with Acme Corp (December 15, 2024)

This uses the vector embeddings stored in Supabase with pgvector. The query flow:

  1. Embed the question
  2. Find similar chunks from meeting transcripts
  3. Pass relevant context to Claude
  4. Generate a grounded response with citations

It's like having a searchable memory of every meeting I've ever had.


Technical Decisions

Why Slack for approvals?

I considered a few options:

  • Email: Too easy to ignore, bad for interactive buttons
  • Dedicated web app: Another tab to check, wouldn't become habit
  • Slack: Already living there all day, native interactive components

Slack's Block Kit makes it easy to build rich interactive messages. The webhook server handles button clicks with proper signature verification.

Why not fully autonomous?

I could have skipped the approval step and let it create tickets automatically. But:

  1. Trust takes time: I wanted to see the proposals first, calibrate the prompts, catch errors
  2. Context Claude misses: Sometimes an action item is actually already done, or is someone else's responsibility
  3. The cost is low: Clicking a button takes 2 seconds. The value is the structured extraction, not the automation.

Over time, I might move toward auto-execution for certain contexts. But the approval step lets me dial confidence up gradually.

Why Supabase + pgvector?

I needed:

  • Structured data storage (meetings, proposals, execution logs)
  • Vector search for the assistant
  • Hosted, managed, cheap

Supabase checks all boxes. The pgvector extension handles embeddings natively. I can query both structured fields and semantic similarity in the same database.


Deployment

The system runs across a few places:

  • Sync agent: GitHub Actions (every 30 min) + launchd on my Mac (for immediate local processing)
  • Webhook server: Fly.io at granola-tools-webhook.fly.dev
  • Database: Supabase hosted PostgreSQL

The launchd service runs at 9am, 1pm, and 6pm local time - after my typical meeting blocks. GitHub Actions provides redundancy.


What I Learned

1. The three-phase pattern is reusable

Sync → Propose → Execute with human approval is a pattern I've now used in multiple projects. It separates concerns cleanly:

  • Sync handles data freshness
  • Propose handles AI analysis
  • Execute handles integrations

2. Context-specific rules matter

Generic prompts produce generic results. The workflow-rules.md file dramatically improved output quality once I encoded my actual preferences per context.

3. Embeddings unlock memory

The assistant feature was almost an afterthought, but it's become one of the most valuable parts. Being able to ask "what did we decide about X?" and get an answer with citations is powerful.

4. Start with approval, remove friction later

Building the approval flow first let me see where the AI was confident vs uncertain. Now I know which contexts could safely be automated and which need human review.


Try It Yourself

The core pattern is straightforward:

  1. Ingest: Pull data from your source (Granola, calendar, email, whatever)
  2. Analyze: Use Claude with structured prompts to extract actionable items
  3. Propose: Post to Slack/Discord/email with interactive elements
  4. Execute: Handle approvals via webhook, call downstream APIs

The specific integrations (Linear, Gmail, Calendar) are just API calls. The magic is in the Claude analysis and the approval workflow that makes it trustworthy.

If you're building something similar, I'd love to hear about it. Reach out at chris@brownridges.com.


Built with Python, Claude (Opus 4.5), Supabase, Slack, and Linear. Deployed on Fly.io and GitHub Actions.