Artificial IntelligenceAnthropicProjects

Project: Build an Automated Meeting Notes Summariser

TT
TopicTrick
Project: Build an Automated Meeting Notes Summariser

Every company runs meetings. Most meetings produce some kind of notes, but those notes are rarely consistent — some are detailed while others are sparse, some capture action items while others do not, and finding agreed decisions from three weeks ago can take longer than it should. An automated meeting notes summariser solves all of these problems at once.

This project builds a system that takes a meeting transcript — from a Zoom, Teams, or Google Meet recording transcription, or from your own notes — and produces a standardised structured summary: the meeting purpose, key discussion points, decisions made, action items with owners and deadlines, and a brief executive summary. Every meeting gets the same quality of documentation automatically.


What We Are Building

The summariser processes a meeting transcript and produces:

  1. Executive summary: 3-4 sentences describing what the meeting was about and the main outcomes
  2. Key discussion points: The substantive topics covered, with important context
  3. Decisions made: Explicit agreements or directions decided in the meeting
  4. Action items: Specific tasks, each with an owner (where identifiable) and deadline (where stated)
  5. Open questions: Issues raised but not resolved, requiring follow-up

Prerequisites

  • Python 3.9 or later
  • pip install anthropic
  • For audio transcription: pip install openai-whisper (or use any transcription service that outputs text)
  • An Anthropic API key set as ANTHROPIC_API_KEY

Complete Implementation

python
1import anthropic 2import json 3from pathlib import Path 4from datetime import datetime 5 6client = anthropic.Anthropic() 7 8 9# ─── Tool Definitions ───────────────────────────────────────────────────────── 10 11MEETING_SUMMARY_TOOL = { 12 "name": "create_meeting_summary", 13 "description": "Create a structured summary of a meeting transcript", 14 "input_schema": { 15 "type": "object", 16 "properties": { 17 "meeting_title": { 18 "type": "string", 19 "description": "A descriptive title for this meeting, e.g. 'Q3 Product Roadmap Planning'" 20 }, 21 "meeting_type": { 22 "type": "string", 23 "enum": ["planning", "review", "standup", "retrospective", "decision", "information", "other"], 24 "description": "The type of meeting" 25 }, 26 "attendees": { 27 "type": "array", 28 "items": {"type": "string"}, 29 "description": "Names of people who spoke or were mentioned as present" 30 }, 31 "executive_summary": { 32 "type": "string", 33 "description": "3-4 sentence overview of the meeting purpose and main outcomes" 34 }, 35 "key_discussion_points": { 36 "type": "array", 37 "items": { 38 "type": "object", 39 "properties": { 40 "topic": {"type": "string"}, 41 "summary": {"type": "string"}, 42 "context": {"type": "string"} 43 }, 44 "required": ["topic", "summary"] 45 }, 46 "description": "Main topics discussed" 47 }, 48 "decisions": { 49 "type": "array", 50 "items": { 51 "type": "object", 52 "properties": { 53 "decision": {"type": "string"}, 54 "rationale": {"type": "string"}, 55 "decided_by": {"type": "string"} 56 }, 57 "required": ["decision"] 58 }, 59 "description": "Decisions or agreements made during the meeting" 60 }, 61 "action_items": { 62 "type": "array", 63 "items": { 64 "type": "object", 65 "properties": { 66 "task": {"type": "string"}, 67 "owner": {"type": "string", "description": "Person responsible, or 'TBD' if not identified"}, 68 "deadline": {"type": "string", "description": "Deadline if stated, otherwise 'Not specified'"}, 69 "priority": {"type": "string", "enum": ["high", "medium", "low"]} 70 }, 71 "required": ["task", "owner", "deadline", "priority"] 72 }, 73 "description": "Specific tasks assigned or agreed upon" 74 }, 75 "open_questions": { 76 "type": "array", 77 "items": {"type": "string"}, 78 "description": "Questions or issues raised but not resolved" 79 }, 80 "next_meeting": { 81 "type": "string", 82 "description": "Next meeting date/time if mentioned, or null" 83 } 84 }, 85 "required": [ 86 "meeting_title", "meeting_type", "executive_summary", 87 "key_discussion_points", "decisions", "action_items" 88 ] 89 } 90} 91 92 93# ─── Core Functions ─────────────────────────────────────────────────────────── 94 95def summarise_meeting(transcript: str, meeting_context: str = "") -> dict: 96 """ 97 Summarise a meeting transcript using Claude. 98 99 Args: 100 transcript: The raw meeting transcript text 101 meeting_context: Optional context like meeting title, date, team name 102 103 Returns: 104 Structured meeting summary as a dictionary 105 """ 106 107 context_block = f"\nMEETING CONTEXT:\n{meeting_context}\n" if meeting_context else "" 108 109 response = client.messages.create( 110 model="claude-sonnet-4-6", 111 max_tokens=4096, 112 tools=[MEETING_SUMMARY_TOOL], 113 tool_choice={"type": "tool", "name": "create_meeting_summary"}, 114 system="""You are an expert meeting notes processor. Your job is to extract clear, 115actionable structured summaries from meeting transcripts. 116 117Be precise about action items — only include concrete tasks that someone explicitly agreed to do. 118Be precise about decisions — only include things that were explicitly agreed upon or approved. 119Open questions should be things genuinely left unresolved, not rhetorical questions from discussion. 120""", 121 messages=[ 122 { 123 "role": "user", 124 "content": f"""{context_block} 125MEETING TRANSCRIPT: 126{transcript} 127 128Create a complete structured summary of this meeting.""" 129 } 130 ] 131 ) 132 133 for block in response.content: 134 if block.type == "tool_use": 135 return block.input 136 137 raise RuntimeError("Summarisation failed") 138 139 140def format_markdown(summary: dict, transcript_date: str = None) -> str: 141 """Format the structured summary as markdown.""" 142 143 date_str = transcript_date or datetime.now().strftime("%Y-%m-%d") 144 145 lines = [ 146 f"# {summary['meeting_title']}", 147 f"**Date:** {date_str} | **Type:** {summary['meeting_type'].title()}", 148 "" 149 ] 150 151 if summary.get("attendees"): 152 lines.append(f"**Attendees:** {', '.join(summary['attendees'])}") 153 lines.append("") 154 155 lines += [ 156 "## Executive Summary", 157 summary["executive_summary"], 158 "" 159 ] 160 161 if summary.get("key_discussion_points"): 162 lines.append("## Key Discussion Points") 163 for point in summary["key_discussion_points"]: 164 lines.append(f"\n### {point['topic']}") 165 lines.append(point["summary"]) 166 if point.get("context"): 167 lines.append(f"*Context: {point['context']}*") 168 lines.append("") 169 170 if summary.get("decisions"): 171 lines.append("## Decisions Made") 172 for d in summary["decisions"]: 173 decision_line = f"- **{d['decision']}**" 174 if d.get("rationale"): 175 decision_line += f" — {d['rationale']}" 176 lines.append(decision_line) 177 lines.append("") 178 179 if summary.get("action_items"): 180 lines.append("## Action Items") 181 lines.append("") 182 lines.append("| Task | Owner | Deadline | Priority |") 183 lines.append("|------|-------|----------|----------|") 184 for item in summary["action_items"]: 185 lines.append(f"| {item['task']} | {item['owner']} | {item['deadline']} | {item['priority'].title()} |") 186 lines.append("") 187 188 if summary.get("open_questions"): 189 lines.append("## Open Questions") 190 for q in summary["open_questions"]: 191 lines.append(f"- {q}") 192 lines.append("") 193 194 if summary.get("next_meeting"): 195 lines.append(f"**Next Meeting:** {summary['next_meeting']}") 196 197 return "\n".join(lines) 198 199 200def process_meeting_file(transcript_path: str, output_dir: str = ".") -> dict: 201 """ 202 Process a meeting transcript file and save outputs. 203 Returns the structured summary. 204 """ 205 path = Path(transcript_path) 206 transcript = path.read_text(encoding="utf-8") 207 208 print(f"Processing: {path.name}") 209 print(f"Transcript length: {len(transcript)} characters") 210 211 summary = summarise_meeting( 212 transcript=transcript, 213 meeting_context=f"File: {path.name}" 214 ) 215 216 # Save structured JSON 217 json_path = Path(output_dir) / f"{path.stem}_summary.json" 218 with open(json_path, "w", encoding="utf-8") as f: 219 json.dump(summary, f, indent=2) 220 221 # Save markdown 222 md_path = Path(output_dir) / f"{path.stem}_summary.md" 223 with open(md_path, "w", encoding="utf-8") as f: 224 f.write(format_markdown(summary)) 225 226 print(f"Summary saved to: {md_path}") 227 print(f"JSON saved to: {json_path}") 228 229 return summary 230 231 232# ─── Example Usage ──────────────────────────────────────────────────────────── 233 234SAMPLE_TRANSCRIPT = """ 235Sarah: Okay, I think everyone is here. Let's get started. Today's agenda is the Q3 marketing budget and the launch timeline for the new product. 236 237Marcus: Before we start, I just wanted to flag that the design team is running about a week behind schedule. 238 239Sarah: Okay, that's important. Let's factor that in when we talk about the launch timeline. So, marketing budget first. We had allocated £45,000 for Q3 digital spend. Given the delay Marcus mentioned, should we hold back some of that spend? 240 241Lisa: I think we should. If we push out the paid social campaign by two weeks, we can save about £8,000 that we could roll into Q4. I'd recommend moving the launch from September 15 to October 1. 242 243Marcus: That works from our side. Two weeks gives us the buffer we need. 244 245Sarah: Agreed. So we're moving the launch to October 1. Lisa, can you update the marketing calendar and brief the agency by end of week? 246 247Lisa: Yes, I'll get that done by Friday. 248 249Sarah: Great. Marcus, what do we need from the product side to get back on track? 250 251Marcus: We need the final copy from the content team by next Tuesday. That's the main blocker. 252 253Sarah: James, you're on content — can you hit Tuesday? 254 255James: Yes, I'll prioritise it. Though I do have a question — do we want the email sequence to follow the same messaging as the landing page, or do we want a different angle? 256 257Sarah: Good question. Let's keep it consistent for now. James, align with Lisa on the messaging this week. One more thing — we still don't have a decision on whether we're doing a webinar as part of the launch. Any thoughts? 258 259Marcus: I'd vote yes but I'm not sure who hosts it. 260 261Lisa: It could be a good channel but I don't want to add scope to an already delayed launch. 262 263Sarah: Let's park the webinar question for now and revisit after the main launch is done. Final question — budget sign-off. Lisa, you need the revised forecast by when? 264 265Lisa: Before end of month — so by June 30. 266 267Sarah: Okay, I'll get finance to approve the revised Q3/Q4 split by then. I think that covers everything. Same team, let's sync again in two weeks — June 28. 268""" 269 270if __name__ == "__main__": 271 summary = summarise_meeting( 272 transcript=SAMPLE_TRANSCRIPT, 273 meeting_context="Marketing team Q3 planning meeting, June 2026" 274 ) 275 276 print(format_markdown(summary, "2026-06-14")) 277 print("\n--- JSON ---") 278 print(json.dumps(summary, indent=2))

Chunk Long Transcripts for Very Long Meetings

For meetings over 2 hours, the transcript can exceed 50,000 tokens. Process these in two passes: first, split the transcript into 30-minute segments and summarise each segment independently. Then use a second Claude call to merge the segment summaries into a final consolidated summary. This two-pass approach keeps each Claude call well within context limits and produces better coherence than trying to process an extremely long transcript in one shot.


    Extending the Project

    • Audio integration: Use OpenAI Whisper or AssemblyAI to transcribe audio/video files before summarisation, creating a fully automated pipeline from recording to structured notes
    • Slack/Teams integration: Post the action items into a Slack channel automatically after each meeting, with owners tagged and items formatted as checkboxes
    • Calendar integration: Extract the next meeting date and create a calendar event automatically using Google Calendar or Microsoft Graph APIs
    • CRM integration: For sales meetings, automatically log the key discussion points and next steps into your CRM against the relevant account

    Summary

    The meeting summariser demonstrates structured extraction at scale. The key design decisions that make it work:

    • Tool use with tool_choice guarantees the output schema — every summary has the same structure regardless of meeting type
    • Granular sub-schemas for action items (task, owner, deadline, priority) enforce the data quality that makes summaries usable downstream
    • Markdown formatting makes summaries immediately readable in email, Slack, Notion, or Confluence
    • JSON output enables downstream automation — feed action items into project management tools, ownership assignments into CRM, and decisions into a searchable archive

    Next project: Project: Build a Code Review Assistant for GitHub PRs.


    This post is part of the Anthropic AI Tutorial Series. Previous post: Project: Build a Customer Support Chatbot with Claude API.