Skip to Content
Google G icon
Sign up with Google

Two Models, Two Jobs: Meeting Notes That Keep the Action Items

Extract action items from a meeting transcript into owner-assigned tasks with two AI models
The readable record and the actionable list are two different documents.

Highlights

  • What you'll build: a pipeline where one model writes the summary and a second model, from a different lab, extracts owners and deadlines from the raw transcript, plus a harness that scores both against a ground-truth list.
  • Prerequisites: Python 3.9+, pip, one API key. No cloud account, no framework.
  • Time: about 25 minutes.
  • Level: beginner to intermediate. Comfortable running a Python script.
  • The measured result: a single cheap model caught 98% of the action items, so the usual scare story is out of date. The pipeline that actually loses them is the one most tools build.
  • The failure that is real: feeding the second model the summary instead of the transcript dropped recall to 71%, and it lost precisely the number, the low-priority item, and the conditional promise.
  • Ends with: a scoring table you can rerun on your own last meeting.

Three weeks after the meeting, the vendor contract renews on the old terms. Nobody pulled the usage numbers, so nobody negotiated. When you go back through the notes, the meeting summary is right there and it is genuinely good: it describes the contract discussion accurately, in a well-written paragraph. What it does not contain is the sentence somebody said out loud, in passing, between two other topics: "I'll get you the numbers Thursday."

The standard explanation for this is that AI summarisers drop action items, and the standard fix is to run a second model. Both halves of that turn out to be worth checking, so this post checks them. You will build the two-model pipeline, and you will also build the small measurement harness that tells you whether it actually helped, because the first result contradicted what the technique is usually sold on.

You need Python 3.9 or newer and one API key. About 25 minutes.

The Claim, and Why It Needs Testing

The argument for splitting the work across two models is genuinely elegant. Summarising means deciding what matters less. Extracting action items means treating one throwaway line, "I'll get you the numbers Thursday", as the most important sentence in the transcript. Those two jobs pull in opposite directions, so asking one model to do both should mean it does one of them badly.

That is a good theory. It also predicts something specific and testable: a single model asked for both should visibly drop action items. So before building anything, measure the thing you are supposedly fixing. Otherwise you spend two models and ten times the money solving a problem you do not have.

Before You Start

mkdir meeting-notes && cd meeting-notes
python3 -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install openai
export KODEKEY_API_KEY="your-key-here"

Generate the key from your KodeKloud account on the KodeKey page. If you have not made an API call before, the first post in this series covers what happens when you do.

Step 1: Get a Transcript That Is Actually Hard

Most demos of this use a tidy transcript with punctuation, clear speaker labels, and action items phrased as action items. Those are easy, and a model will ace them. Real transcripts from Zoom or Google Meet are not like that: no punctuation, filler words, people talking over each other, and a long tangent in the middle where nothing gets decided.

Save this as transcript.txt. It is written in raw meeting-tool style on purpose, and it hides commitments in the places real meetings hide them: a number said once, an item explicitly called "not urgent", a conditional promise, a task someone assigns to themselves in an aside, and one more thing after the host has already said goodbye.

Dana Okay I think we're all here um let's just start I've got a hard stop at eleven so checkout latency first Marcus you were digging into this

Marcus yeah so p99 is four point two seconds it was nine hundred milliseconds a week ago and it's the connection pool we cap at fifty and the recommendations call holds a connection open while it waits on the model so everything else just queues up behind it

Dana can we make the pool bigger

Marcus short term yes I'll push it to two hundred tonight once traffic drops off that buys us maybe a month the actual fix is not holding the connection during that call

Priya sorry was that call not supposed to be async I thought that was the whole point

Marcus it was supposed to be it shipped sync because async needed the queue and the queue wasn't done

Dana so the pool thing is a band aid understood Priya what would it take to get the queue finished

Priya two weeks if I drop the migration a month if I don't

Dana don't drop the migration where is that anyway

Priya staging is fully cut over production is waiting on the runbook which is on me I'll have it out Thursday

Dana Thursday's fine um Sam you had vendor stuff

Sam yeah two things the model vendor contract renews on the thirtieth and nobody's actually looked at what we're using and the second thing is a bit awkward we're still running the recommendations service on the founder's personal API key

Dana sorry we're what

Sam it was a prototype it went to prod it's been like that since March

Dana that cannot still be true at the end of the month

Sam agreed it's like a two hour job it's just never been anybody's job

Marcus I can pick it up not this week but next week I could

Dana okay next week Sam can you get me the usage numbers before we renew so we're not going in blind

Sam yep I'll get you the numbers Thursday

Dana great um okay while we're on infra can we talk about the staging environment for a second because I keep hearing it's slow

Priya it's the old instance type the database is still on the previous generation that's why the migration tests crawl it's not urgent though

Dana put it on the board anyway

Priya will do

Marcus can I go back to latency for one second

Dana yeah go

Marcus if p99 is still bad after the pool bump then it's not the pool it's the model call itself and at that point we need to go to the vendor about their latency I'll open a ticket with them if that's where we land

Dana understood okay um now the thing I actually wanted to spend time on which is the roadmap conversation because we've been going back and forth on this for three weeks and I want to close it out today so the question on the table is whether we do the search rewrite this quarter or push it to next

Sam I still think search is the wrong priority honestly the numbers don't support it we did the analysis in April and search was responsible for what four percent of drop off

Priya it was closer to six but yeah the point stands

Sam six okay six but checkout is like thirty percent of drop off so if we're optimizing for revenue it's obviously checkout

Marcus I don't disagree but search is the thing customers complain about in the reviews so there's a brand cost that doesn't show up in the funnel

Dana that's fair

Sam it's fair but it's not measurable and I'd rather move a number we can see

Priya can I say something slightly annoying which is that the search rewrite as scoped is not actually a rewrite it's a re index and a relevance tuning pass and that's like three weeks not a quarter

Sam wait is that true because the estimate I saw said a quarter

Priya the estimate you saw included the new autocomplete service which is a separate thing that got bundled in

Dana who bundled it

Priya I think it was in the original doc from January

Sam okay that changes things a lot honestly if it's three weeks I'd just do it

Marcus yeah if it's three weeks that's a different conversation

Dana okay so can somebody go and actually confirm the scope because we've now spent three weeks arguing about a number that might be wrong

Priya I can split the doc into the two pieces and re estimate

Dana how long

Priya couple of days

Dana do that and then we'll pick it up next week um okay what else was on my list oh the on call rotation

Marcus what about it

Dana we've got two people on the rotation and one of them is you and you're also the only person who understands the payment service which is not a great place to be

Marcus no it's not

Dana so I want a second person cross trained on payments I don't need it done this month but I want to know who it is

Priya I mean I could but not until the migration's done

Dana that's fine that's what I wanted to hear we'll say Priya after the migration

Marcus I should probably write down how the reconciliation job works because that's genuinely only in my head

Dana yes please do that

Marcus okay

Dana um alright and then the last thing and then I'll let you all go the board deck is Friday and I need one slide on why checkout got slow and what we did about it Marcus can you give me three bullets Thursday afternoon

Marcus Thursday afternoon yeah

Dana perfect okay thanks everyone

Priya oh wait Dana before you drop the analytics thing you asked about

Dana oh right yes

Priya do you still need that broken out by region or was the total enough

Dana broken out by region if it's not painful

Priya it's not painful I'll send it over with the runbook

Dana perfect thanks okay bye everyone

Now read it yourself and write down every commitment before any model sees it. This step is not optional, and it is the difference between measuring and guessing. There are eleven:

  1. Marcus: push the connection pool to 200, tonight
  2. Priya: production migration runbook, Thursday
  3. Marcus: take the recommendations service off the founder's personal key, next week
  4. Sam: vendor usage numbers before the renewal, Thursday
  5. Priya: put the staging database on the board
  6. Marcus: open a vendor ticket if p99 is still bad after the pool bump
  7. Priya: split the search doc in two and re-estimate, couple of days
  8. Priya: cross-train on payments, after the migration
  9. Marcus: document the reconciliation job
  10. Marcus: three bullets for the board deck, Thursday afternoon
  11. Priya: analytics broken out by region, with the runbook

Step 2: Test the Thing You Are Supposedly Fixing

One model, one prompt, both jobs. The cheapest model in the list.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["KODEKEY_API_KEY"],
    base_url="https://api.ai.kodekloud.com/v1",
)

transcript = open("transcript.txt").read()

reply = client.chat.completions.create(
    model="google/gemini-3.1-flash-lite",
    messages=[{"role": "user", "content":
               "Summarise this meeting and list the action items.\n\n" + transcript}],
)
print(reply.choices[0].message.content)
print("cost:", reply.usage.cost)

Run it, then check the output against your eleven.

It gets them. Over five runs it caught 54 of 55, at about a tenth of a cent per run. The one it missed once was the search re-estimate, buried in the middle of the long roadmap argument. A stronger model asked to do both jobs caught 55 of 55.

So the premise is out of date. A modern model asked to summarise a meeting and list the action items does not drop half of them. If you have been avoiding this because of a blog post from a few years ago, go and re-measure on your own transcripts before you build anything more complicated. The cheap single-model version is a genuinely good default.

Which leaves a real question: if one model is fine, where does the second one actually earn its money?

Step 3: The Pipeline That Actually Loses Your Action Items

Here is the version most tools build, and the one that felt natural when I started this: summarise first, then extract the tasks from the summary. It seems obviously correct. The summary is shorter, cheaper to process, and already contains the important parts.

summary = ask("google/gemini-3.1-flash-lite",
              "Summarise this meeting in 5 bullets.\n\n" + transcript)

# The second model only ever sees the summary.
items = ask("gpt-5.4",
            "List every commitment somebody made. Return JSON with keys "
            "owner, task, due.\n\n" + summary)

Recall drops to 71%. Not because either model is bad, but because of what a summary is. Look at the summary the first model produced. It is a good piece of writing:

*   Addressing Checkout Latency: P99 latency has spiked due to a connection
    pool bottleneck. Marcus will increase the pool size temporarily tonight,
    with a permanent fix involving an asynchronous queue to be implemented
    after Priya completes a pending database migration.
*   Vendor and Security Risks: The team identified that the recommendation
    service is still running on a founder's personal API key. Marcus will take
    over the key migration next week, and Sam will provide usage data by
    Thursday to inform the upcoming vendor contract renewal.
*   Search Rewrite Scope: [...] Priya will re-estimate the scoped-down project
    for review next week.
*   Team Resiliency: [...] Priya will cross-train on the payment service once
    her current migration is complete. Marcus will document the payment
    reconciliation process [...]
*   Deliverables: Several items are due by Thursday, including the runbook and
    regional analytics (Priya), usage numbers (Sam), and a summary slide on the
    checkout latency incident (Marcus) for Friday's board deck.

Accurate, readable, and it has quietly destroyed three action items:

  • The number is gone. "Increase the pool size temporarily tonight." Two hundred is not in there. A summary keeps the shape of a decision and discards the parameter.
  • The item somebody called unimportant is gone. Priya said the staging database was "not urgent", so it did not make the top five bullets. Dana still asked for it on the board.
  • The conditional is gone. "I'll open a ticket with them if that's where we land" never appears, because a summary describes what happened and that has not happened yet. Conditional commitments are the first thing a summariser cuts and the thing that most reliably falls through in real life.

Then it gets worse in a way that is easy to miss. Asked to extract commitments from that summary, the second model produced this:

{
  "owner": "Marcus",
  "task": "implement a permanent fix for checkout latency involving an asynchronous queue",
  "due": "after Priya completes the pending database migration"
}

Nobody said that. Go back to the transcript: Marcus says the real fix is not holding the connection open, and Priya is the one who owns the queue. The summary's phrasing put "Marcus" and "permanent fix" in the same sentence, and the second model, having no transcript to check against, did the reasonable thing and turned it into a task with the wrong owner.

That is the actual argument for two models, and it is stronger than the one usually made: a model cannot catch what the previous step already threw away, and it cannot tell the difference between what was said and what a summary implied.

πŸš€ Hands-On

Two models from two labs, without two accounts

KodeKey puts GPT, Claude, Gemini, DeepSeek and more behind one key and one endpoint, so the second model in this pipeline is a string change rather than a second signup. Built for learning and prototyping, included with your KodeKloud subscription.

Get your KodeKey β†’

Step 4: Point the Second Model at the Raw Transcript

The fix is one variable. The extractor reads the transcript, not the summary, and it is told not to summarise.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["KODEKEY_API_KEY"],
    base_url="https://api.ai.kodekloud.com/v1",
)
transcript = open("transcript.txt").read()


def ask(model, prompt):
    reply = client.chat.completions.create(
        model=model, messages=[{"role": "user", "content": prompt}]
    )
    return reply.choices[0].message.content


# Job 1: the readable record. Cheap model, because prose is the easy job.
summary = ask(
    "google/gemini-3.1-flash-lite",
    "Summarise this meeting in 5 bullets.\n\n" + transcript,
)

# Job 2: the list you act on. Different lab, reading the transcript from
# scratch, explicitly told not to summarise.
items = ask(
    "gpt-5.4",
    "Read this raw meeting transcript. Do not summarise it. List every "
    "commitment somebody made, including tentative, deferred and conditional "
    "ones. Return JSON: a list of objects with keys owner, task, due. Use null "
    "for an unknown due date.\n\n" + transcript,
)

print(summary)
print(items)

Two details in that prompt are doing real work. "Do not summarise it" stops the model drifting back into the job it does by default. "Including tentative, deferred and conditional ones" is what rescues the vendor ticket, because without it a model applies its own judgement about what counts as a real commitment, and a hypothetical does not look like one.

Here is what came back, unedited:

[
  {"owner": "Marcus", "task": "Push the connection pool cap to 200 as a short-term fix for checkout latency.", "due": "tonight once traffic drops off"},
  {"owner": "Priya", "task": "Have the production runbook for the migration out.", "due": "Thursday"},
  {"owner": "Marcus", "task": "Pick up the work to replace the founder's personal API key for the recommendations service.", "due": "next week"},
  {"owner": "Sam", "task": "Get Dana the usage numbers before the model vendor contract renewal.", "due": "Thursday"},
  {"owner": "Priya", "task": "Put the staging environment issue on the board.", "due": null},
  {"owner": "Marcus", "task": "Open a ticket with the model vendor about latency if p99 is still bad after the pool bump.", "due": null},
  {"owner": "Priya", "task": "Split the search roadmap doc into re-index/relevance tuning and autocomplete pieces and re-estimate them.", "due": "couple of days"},
  {"owner": "Priya", "task": "Become the second person cross-trained on payments after the migration is done.", "due": null},
  {"owner": "Marcus", "task": "Write down how the payment reconciliation job works.", "due": null},
  {"owner": "Marcus", "task": "Give Dana three bullets for the board deck on why checkout got slow and what was done about it.", "due": "Thursday afternoon"},
  {"owner": "Priya", "task": "Send Dana the analytics broken out by region.", "due": "with the runbook"}
]

Eleven items, correct owners, nothing invented. Note what it did with the deadlines: it kept "tonight once traffic drops off" and "with the runbook" as spoken, rather than inventing a date. null where nobody said one. That is the behaviour you want, because a made-up deadline is worse than a missing one.

Step 5: Score It Instead of Trusting It

Reading two outputs and forming an impression is not measurement. The harness is small: a ground-truth list, a keyword check, and a loop.

TRUTH = [
    ("Marcus: bump pool to 200",          [["pool"], ["200", "two hundred"]]),
    ("Priya: migration runbook Thu",      [["runbook"]]),
    ("Marcus: take over founder API key", [["api key"], ["replac", "rotat", "personal", "founder", "take over"]]),
    ("Sam: vendor usage numbers Thu",     [["usage"]]),
    ("Priya: staging DB onto the board",  [["staging"], ["board"]]),
    ("Marcus: vendor ticket IF p99 bad",  [["ticket"]]),
    ("Priya: re-estimate the search doc", [["doc", "scope", "estimat"], ["split", "re-estimat", "re estimat"]]),
    ("Priya: cross-train on payments",    [["cross train", "cross-train"]]),
    ("Marcus: document reconciliation",   [["reconciliation"]]),
    ("Marcus: 3 bullets board deck Thu",  [["bullet", "board deck", "slide"]]),
    ("Priya: analytics split by region",  [["region"]]),
]


def score(text):
    low = text.lower()
    return [all(any(s in low for s in group) for group in groups)
            for _, groups in TRUTH]

One trap to know about before you loop. Repeating an identical request to the gateway can return a cached response, byte for byte, so five identical runs collapse into one measurement and your results look far more stable than they are. Vary something per run:

for run in range(5):
    tag = f"\n\n(session {run})"
    text = ask(model, prompt + tag)

Five runs of each approach, scored against the eleven, gives this.

Pipeline Action items caught Cost per run
Cheap model, both jobs 98% (54/55) $0.00105
Strong model, both jobs 100% (55/55) $0.01533
Two models, extractor reads the raw transcript 100% (55/55) $0.01015
Two models, extractor reads the summary 71% (39/55) $0.00717
Extractor reads a summary told to keep commitments verbatim 95% (52/55) $0.00769

The last row is there because the summary-chained pipeline deserved a fair hearing rather than a straw man. Tell the summariser to preserve every commitment, owner, number and deadline verbatim, and most of the loss comes back: 95%. It is a real fix if you are stuck with a summary as your only input.

It still does not fully work, and the item it keeps losing is the interesting one. The conditional vendor ticket survived only three runs in five even with that instruction, while every approach that read the raw transcript caught it in all five. You can ask a summary to carry the commitments and it will mostly comply. You cannot reliably ask it to carry the things that have not happened yet.

What This Actually Tells You

Three conclusions, in the order they matter.

Do not chain the second model onto the summary. This is the whole finding. It is the pipeline that looks most sensible, it saves a little money, and it is the only configuration tested here that fails badly.

One cheap model is a good default. At 98% and a tenth of a cent, the single-model version is the right answer for a lot of teams, and pretending otherwise to sell a technique would be dishonest. The two-model split earns its extra cost when you need structured output with owners and deadlines you can push into a tracker, when the conditional commitments matter, and when you want the check to be genuinely independent.

Independence is the reason to cross labs. Using a second model from the same company as the first buys you less than it looks, because models from one lab share training data and tend to share blind spots. The extractor here comes from a different company than the summariser on purpose. That idea is the foundation of the next two posts in this series, where a model checks another model's work and where four models disagreeing turns out to be a diagnosis of your own data rather than of the models.

Common Errors and Fixes

Symptom Cause Fix
Repeated runs give identical results The gateway can return a cached response for an identical request. Add a per-run nonce to the prompt, as in step 5.
TypeError: unsupported format string on usage.cost Not every model returns a cost field on the usage object. Use getattr(reply.usage, "cost", None) or 0.0.
403 user not allowed to access model The model exists on the endpoint but is not included in your plan. Pick another model from the two labs you are comparing.
JSON output wrapped in a code fence Models often return JSON inside markdown fences. Strip the fence before json.loads, or use the SDK's structured output support.
Recall looks worse than the output actually is Your keyword matcher is stricter than your ground truth. A correct item phrased differently scores as a miss. Read the raw output whenever a cell says zero. This caught a false failure while writing this post.

That last row is worth dwelling on. The first version of this harness reported that the two-model pipeline had missed the staging database item entirely, five runs out of five. It had not. It had written "Put the staging environment issue on the board" while the matcher was looking for "staging database". A scoring bug that makes your preferred approach look bad is lucky; the same bug in the other direction would have shipped a wrong number in this post.

Clean Up

Nothing runs in the cloud.

deactivate
rm -rf meeting-notes

If you ran this on a real transcript, delete that too, and think about where it came from before you run the next one. A meeting transcript is one of the more sensitive documents a company produces.

Next Steps

Run it on your own last meeting, and write the ground truth yourself first. The value is not in the code, which you have already seen in full, but in finding out which commitments your own meetings hide. In practice they cluster: conditionals, anything somebody labels as unimportant, and anything agreed after the host says "last thing".

From there, two directions. Add structured output so the extractor is constrained to a schema instead of asked politely for JSON. Or point the same pattern somewhere else: pull request discussions, incident channels, and customer calls all have the same shape, where the readable record and the actionable list are two different documents that most tools try to produce in one pass.

For the concepts underneath, the AI Learning Path covers prompting and evaluation in order, and the official KodeKey examples repo has runnable starting points if you would rather begin from working code.

Conclusion

The reason to use two models is not that one model is careless. It is that a summary is a lossy format by design, and the moment you build on top of it, everything it discarded is gone for good. Keep the summary for humans to read. Extract the commitments from the source, with a model that has no idea what the first one decided was important.

The measurement matters as much as the pipeline. Eleven items, five runs, one small scoring function, and the numbers immediately contradicted the technique's usual sales pitch while pointing at a bigger problem nobody mentions. That is a cheap habit to build, and the next post makes it the whole subject: the same prompt, run across five models, scored on checks you write yourself.

Try It on Your Own Last Meeting

The pipeline in this post needs two models from two different companies, and the only reason it takes twenty-five minutes rather than a procurement cycle is that both answered to one key. KodeKey gives you a few dozen models from about ten labs behind one OpenAI-compatible endpoint, included with your KodeKloud subscription, and built for exactly this sort of prototyping rather than production traffic. Generate a key and run step 4 against a transcript from your own week.

If you want the fuller picture first, how LLM APIs work covers tokens, context windows, and where the cost comes from, and one API key for multiple LLMs explains why using two labs normally costs you two accounts. When you want to build the judgement rather than the script, the AI Learning Path is the structured route.


FAQs

Q1: Do I really need two different companies' models?

For this task, the transcript matters more than the lab. Both single-model arms did fine. Cross-lab independence matters most when the second model is checking the first one's work rather than reading the source itself, which is the case in the next post. Here it is a reasonable default rather than the thing doing the work.

Q2: Why not just use a better prompt on one model?

You can, and it gets you to 100% on this transcript. The two-model version buys you a clean separation: a prose summary for people, and a JSON list with owners and deadlines for your tracker. If you only need the prose, one model is cheaper and simpler.

Q3: Will this work on a two-hour meeting?

The limit is the context window. A two-hour transcript can run past what some models accept, and you will get an explicit error rather than silent truncation. Split it by agenda item and extract per chunk, then concatenate. Do not summarise the chunks first, for the reason this whole post is about.

Q4: How accurate are the deadlines?

They are as accurate as the meeting. The extractor kept "with the runbook" and "tonight once traffic drops off" because that is what people said, and returned null where nobody committed to a date. Resist the urge to prompt it into producing real dates; an invented deadline is worse than an obvious gap.

Q5: Can I run this on customer calls or recorded meetings?

Check your recording consent and data handling policy first, and confirm where the transcript is allowed to go. A learning key is fine for a transcript you wrote or one from your own internal meeting; it is the wrong tool for regulated customer data.

Q6: Is this production-ready?

No. It is a prototype pattern on a learning key, with no retries, no schema validation, and no persistence. Build it to understand the shape, then harden it and move to a provider account with production terms if you come to depend on it.

Nimesha Jinarajadasa Nimesha Jinarajadasa
Nimesha Jianrajadasa is a DevOps & Cloud Consultant, K8s expert, and instructional content strategist-crafting hands-on learning experiences in DevOps, Kubernetes, and platform engineering.

Subscribe to Newsletter

Join me on this exciting journey as we explore the boundless world of web design together.