[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build an AI Meeting Summarizer with Action Items

Build an AI Meeting Summarizer with Action Items

Auto-generate meeting summaries, extract action items with owners and deadlines, and sync to project management tools — saving 5 hours/week per team and ensuring nothing falls through the cracks.

#queue#redis#jobs#background#worker
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 7 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

vercel-ai-sdk

v

Not yet scored
View skill
>

bull-mq

v1.0.0

You are an expert in BullMQ, the high-performance job queue for Node.js built on Redis. You help developers build reliable background processing systems with delayed jobs, rate limiting, prioritization, repeatable cron jobs, job dependencies, concurrency control, and dead-letter handling — powering email sending, image processing, webhook delivery, report generation, and any async workload.

93/100 quality
3.00× impact
SAFE
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
$

The Problem

A 50-person company has 200 meetings/week. Meeting notes are either not taken, taken poorly, or taken by someone who then can't participate. Action items live in people's heads — 40% are forgotten. "Didn't we discuss this last week?" is said daily. The PM spends 5 hours/week manually writing summaries and chasing action items. When someone misses a meeting, they have no way to catch up except asking a colleague to rehash it.

Step 1: Transcript Processor

typescript
// src/meetings/processor.ts
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

const MeetingSummary = z.object({
  title: z.string(),
  date: z.string(),
  duration: z.string(),
  participants: z.array(z.string()),
  summary: z.string().max(500),
  keyTopics: z.array(z.object({
    topic: z.string(),
    discussion: z.string(),
    outcome: z.enum(['decided', 'needs_followup', 'informational', 'tabled']),
    decision: z.string().optional(),
  })),
  actionItems: z.array(z.object({
    description: z.string(),
    owner: z.string(),
    deadline: z.string().optional(),
    priority: z.enum(['high', 'medium', 'low']),
    context: z.string(), // why this action item exists
  })),
  openQuestions: z.array(z.string()),
  nextSteps: z.array(z.string()),
  sentiment: z.enum(['productive', 'neutral', 'contentious', 'unfocused']),
});

export async function summarizeMeeting(transcript: string, metadata: {
  meetingTitle?: string;
  participants: string[];
  scheduledDuration: string;
}): Promise<z.infer<typeof MeetingSummary>> {
  const { object } = await generateObject({
    model: openai('gpt-4o'),
    schema: MeetingSummary,
    prompt: `Summarize this meeting transcript. Extract every action item with a clear owner.

Meeting: ${metadata.meetingTitle ?? 'Untitled Meeting'}
Scheduled duration: ${metadata.scheduledDuration}
Participants: ${metadata.participants.join(', ')}

Transcript:
${transcript}

Rules:
- Summary should be 2-3 sentences, high-level
- Each topic should capture the key discussion points and outcome
- Action items MUST have an owner (the person who said "I'll do X" or was assigned)
- If a deadline was mentioned, include it
- Open questions are things raised but not resolved
- Be objective — don't editorialize
- If people talked over each other or went in circles, mark sentiment as "unfocused"`,
  });

  return object;
}

Step 2: Action Item Tracker

typescript
// src/meetings/action-tracker.ts
import { Pool } from 'pg';
import { Redis } from 'ioredis';

const db = new Pool({ connectionString: process.env.DATABASE_URL });
const redis = new Redis(process.env.REDIS_URL!);

export async function saveActionItems(
  meetingId: string,
  actionItems: Array<{
    description: string;
    owner: string;
    deadline?: string;
    priority: string;
    context: string;
  }>
): Promise<void> {
  for (const item of actionItems) {
    const id = crypto.randomUUID();
    await db.query(`
      INSERT INTO action_items (id, meeting_id, description, owner, deadline, priority, context, status, created_at)
      VALUES ($1, $2, $3, $4, $5, $6, $7, 'open', NOW())
    `, [id, meetingId, item.description, item.owner, item.deadline, item.priority, item.context]);
  }
}

// Weekly digest: which action items are overdue?
export async function getOverdueItems(): Promise<Array<{
  owner: string;
  items: Array<{ description: string; meetingTitle: string; deadline: string; daysPast: number }>;
}>> {
  const { rows } = await db.query(`
    SELECT ai.owner, ai.description, ai.deadline, m.title as meeting_title,
           EXTRACT(DAY FROM NOW() - ai.deadline::date) as days_past
    FROM action_items ai
    JOIN meetings m ON ai.meeting_id = m.id
    WHERE ai.status = 'open' AND ai.deadline IS NOT NULL AND ai.deadline::date < NOW()
    ORDER BY ai.owner, days_past DESC
  `);

  const byOwner = new Map<string, any[]>();
  for (const row of rows) {
    if (!byOwner.has(row.owner)) byOwner.set(row.owner, []);
    byOwner.get(row.owner)!.push({
      description: row.description,
      meetingTitle: row.meeting_title,
      deadline: row.deadline,
      daysPast: parseInt(row.days_past),
    });
  }

  return [...byOwner.entries()].map(([owner, items]) => ({ owner, items }));
}

// Sync to Jira/Linear
export async function syncToProjectManagement(
  actionItem: { description: string; owner: string; deadline?: string; priority: string }
): Promise<void> {
  // Create task in Linear/Jira
  if (process.env.LINEAR_API_KEY) {
    await fetch('https://api.linear.app/graphql', {
      method: 'POST',
      headers: {
        Authorization: process.env.LINEAR_API_KEY!,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: `mutation { issueCreate(input: {
          title: "${actionItem.description}",
          assigneeId: "${actionItem.owner}",
          priority: ${actionItem.priority === 'high' ? 1 : actionItem.priority === 'medium' ? 2 : 3},
          ${actionItem.deadline ? `dueDate: "${actionItem.deadline}"` : ''}
        }) { success } }`,
      }),
    });
  }
}

Step 3: API and Webhook

typescript
// src/api/meetings.ts
import { Hono } from 'hono';
import { summarizeMeeting } from '../meetings/processor';
import { saveActionItems } from '../meetings/action-tracker';
import { Pool } from 'pg';

const app = new Hono();
const db = new Pool({ connectionString: process.env.DATABASE_URL });

// Webhook from Zoom/Google Meet/Fireflies
app.post('/v1/meetings/transcript', async (c) => {
  const { transcript, meetingTitle, participants, duration, recordingUrl } = await c.req.json();
  const meetingId = crypto.randomUUID();

  // Save raw transcript
  await db.query(`
    INSERT INTO meetings (id, title, participants, duration, transcript, recording_url, created_at)
    VALUES ($1, $2, $3, $4, $5, $6, NOW())
  `, [meetingId, meetingTitle, participants, duration, transcript, recordingUrl]);

  // Generate summary
  const summary = await summarizeMeeting(transcript, {
    meetingTitle,
    participants,
    scheduledDuration: duration,
  });

  await db.query(`UPDATE meetings SET summary = $1 WHERE id = $2`, [JSON.stringify(summary), meetingId]);

  // Save and sync action items
  await saveActionItems(meetingId, summary.actionItems);
  for (const item of summary.actionItems) {
    await syncToProjectManagement(item).catch(() => {});
  }

  return c.json({ meetingId, summary });
});

// Search past meetings
app.get('/v1/meetings/search', async (c) => {
  const query = c.req.query('q');
  const { rows } = await db.query(`
    SELECT id, title, created_at, summary->>'summary' as summary
    FROM meetings
    WHERE to_tsvector('english', transcript) @@ plainto_tsquery('english', $1)
    ORDER BY created_at DESC LIMIT 20
  `, [query]);

  return c.json({ results: rows });
});

export default app;

import { syncToProjectManagement } from '../meetings/action-tracker';

Results

  • Note-taking: fully automated — nobody manually writes meeting notes
  • Action item capture: 95% (was ~60% manually, 40% forgotten)
  • PM time saved: 5 hours/week freed from summary writing
  • Overdue tracking: weekly digest catches forgotten items — completion rate up 35%
  • "What did we decide?": searchable meeting archive answers in seconds
  • Missed meeting catch-up: read summary in 2 minutes instead of 30-minute rehash
  • Project management sync: action items auto-create Linear/Jira tasks