DEV Community

Robert Breen
Robert Breen

Posted on Originally published at learn.ynteractive.com

Claude: Meeting Minutes as Structured JSON (Decisions, Owners, Due Dates) With a Schema and a Short Python Script

Every office I work with has the same Monday problem. The meeting ends, everyone leaves with a slightly different idea of what was decided, and nobody is quite sure who was supposed to call the repair company. A week later someone asks "did we decide that?" and there's nothing to point to.

That's what meeting minutes are for: the short written record of who attended, what was decided, who owns each next step and by when, and what's still open. Not every word said. Just what the group needs to act on.

AI makes minutes fast. Paste a transcript into Claude with a clear request and you get decisions and action items in seconds. My Claude meeting transcript guide covers that chat version step by step.

This post is for the next step. When the minutes need to go somewhere, a task list, a Google Sheet, a project tool, a follow-up email that a script sends, a nicely formatted chat answer isn't enough. You want the same content as JSON, in a shape your code can rely on.

The rule that matters most

Before any code: the request in the chat version has one line that does more than the rest combined.

Only use what's in the transcript. If an owner or date isn't said, write "Not stated".

Without it, a model will fill gaps. It will decide that Luis probably owns the oven call because Luis mentioned the oven. It will guess "Friday" for a quote nobody gave a date for. With it, the gaps stay visible, and a person who was in the room fills them in.

In JSON form I keep exactly the same convention. owner and due_date are always strings, and the literal value "Not stated" means nobody said it. That's easier to filter than nulls and it reads the same in a spreadsheet.

The schema

{
  "type": "object",
  "properties": {
    "meeting_title": { "type": "string" },
    "summary": { "type": "string" },
    "attendees": { "type": "array", "items": { "type": "string" } },
    "decisions": { "type": "array", "items": { "type": "string" } },
    "action_items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "task": { "type": "string" },
          "owner": { "type": "string" },
          "due_date": { "type": "string" },
          "source_quote": { "type": "string" }
        },
        "required": ["task", "owner", "due_date", "source_quote"],
        "additionalProperties": false
      }
    },
    "open_questions": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["meeting_title", "summary", "attendees", "decisions", "action_items", "open_questions"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

Two fields are there for checking, not for reading:

  • source_quote is a short line from the transcript that supports the action item. When you check the minutes, you search the transcript for it. If you can't find it, the item is suspect.
  • attendees only lists people who spoke or were named as present. For a formal board, your bylaws decide what the official minutes must record, and the chair or secretary approves them. This is a draft for them, not a replacement.

The Python call

This uses the official Anthropic SDK (pip install anthropic) and structured outputs: output_config with a json_schema format makes Claude's reply valid JSON for that schema.

import json
import sys

import anthropic

SCHEMA = json.load(open("minutes_schema.json", encoding="utf-8"))

INSTRUCTIONS = """You turn a meeting transcript into meeting minutes.
Only use what is in the transcript. Do not infer owners or dates.
If an owner or a due date is not said, write exactly "Not stated".
Decisions are things the group agreed, not ideas someone raised.
The summary is three sentences at most.
For each action item, source_quote is a short exact quote from the transcript that supports it."""

def minutes_from_transcript(transcript: str) -> dict:
    client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from the environment
    response = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=16000,
        system=INSTRUCTIONS,
        output_config={
            "effort": "low",
            "format": {"type": "json_schema", "schema": SCHEMA},
        },
        messages=[{
            "role": "user",
            "content": f"<transcript>\n{transcript}\n</transcript>",
        }],
    )
    if response.stop_reason == "refusal":
        raise RuntimeError("Claude declined this request")
    if response.stop_reason == "max_tokens":
        raise RuntimeError("Output was cut off; split the transcript")
    text = next(b.text for b in response.content if b.type == "text")
    return json.loads(text)

if __name__ == "__main__":
    transcript = open(sys.argv[1], encoding="utf-8").read()
    minutes = minutes_from_transcript(transcript)
    print(json.dumps(minutes, indent=2, ensure_ascii=False))

    missing = [a for a in minutes["action_items"]
               if "Not stated" in (a["owner"], a["due_date"])]
    if missing:
        print(f"\n{len(missing)} action item(s) need an owner or a date:", file=sys.stderr)
        for a in missing:
            print(f"  - {a['task']} (owner: {a['owner']}, due: {a['due_date']})", file=sys.stderr)
Enter fullscreen mode Exit fullscreen mode

Save the schema above as minutes_schema.json, export your API key, and run:

python minutes.py monday-meeting.txt > minutes.json
Enter fullscreen mode Exit fullscreen mode

I set effort to low because this is extraction, not reasoning. If the minutes miss things on long, messy meetings, raise it to medium and compare on the same transcript.

What comes back

For a bakery's Monday team meeting (the made-up Harbor Street Bakery from my ChatGPT lesson), the shape looks like this:

{
  "meeting_title": "Monday team meeting",
  "summary": "The team planned holiday production and agreed to increase pumpkin rolls. Holiday pie orders will be capped. Two follow-ups have no date yet.",
  "attendees": ["Luis"],
  "decisions": [
    "Double the pumpkin rolls",
    "Cap holiday pies at sixty a day"
  ],
  "action_items": [
    {
      "task": "Send the library a catering quote",
      "owner": "Not stated",
      "due_date": "Not stated",
      "source_quote": "we still owe the library a quote"
    },
    {
      "task": "Call the repair company about the oven",
      "owner": "Luis",
      "due_date": "Not stated",
      "source_quote": "I can call them about the oven"
    }
  ],
  "open_questions": [
    "Whether to renew the van lease",
    "Whether to hire extra counter help for the holidays"
  ]
}
Enter fullscreen mode Exit fullscreen mode

That's illustrative output, not a recorded run, and the quotes are made up for the example. Only names someone says in the transcript belong in attendees, which is why the list is short. The point is the two "Not stated" dates. In the lesson, nobody said when the library quote or the oven call would happen, so the honest answer is that nobody said. A person who was there fills them in ("the quote is due Wednesday, Luis calls today"), and only then do the minutes go out.

Where the JSON goes next

Because the shape is fixed, the next step is ordinary code:

  • A sheet or task list: one row per action item, with the Not stated ones highlighted for whoever ran the meeting.
  • A follow-up email: render decisions and action items into a short recap. Send it the same day, while people can still correct it.
  • A check before anything is sent: refuse to send if any action item still says "Not stated".
def ready_to_send(minutes: dict) -> bool:
    return all(a["owner"] != "Not stated" and a["due_date"] != "Not stated"
               for a in minutes["action_items"])
Enter fullscreen mode Exit fullscreen mode

Things to settle before you paste a transcript

Client meetings need more care than internal ones. Check your company's rules on AI tools first, use a plan or API account your company has approved, and leave out account numbers and anything private the minutes don't need. A one-hour transcript is small; a very long one may need splitting by meeting or topic.

And keep a person in the loop. AI can turn a transcript into decisions in seconds. It can't know what was meant but never said.

Try it free

The Claude meeting transcript guide is the no-code version: the exact request, upload vs paste, file limits and what to check. For the definition and a minutes example, see What are meeting minutes?.

Practice it step by step, free: https://learn.ynteractive.com/content/blog/claude-summarize-meeting-transcript

Top comments (0)