Skip to content

Extracting Action Items From an Email Thread

10 min read · updated August 11, 2026

Minutes name the owner because a secretary wrote them down. Email does not. The commitments in a thread are made in the first and second person, which means the owner is a fact about the envelope rather than about the sentence — and sometimes it is not a fact at all.

The owner is usually not in the sentence

The most common form of a commitment in email is “I’ll send the revised figures on Friday.” There is no name in it. The owner is whoever sent the message, which you read out of the From header — not out of the text, and not out of the signature block, which is frequently somebody else’s because people send on behalf of a team address.

This sounds obvious until you notice what it implies about the input. The owner is only correct if the sentence you are looking at came from the message whose From header you paired it with. Quoted history breaks that pairing completely: a reply from Reyes containing twelve lines of Okafor’s earlier message will attribute Okafor’s “I’ll send the figures” to Reyes, and it will do it with total confidence because nothing about the extraction is uncertain. Every commitment in a thread gets re-attributed to the last person who quoted it.

So this extraction depends entirely on the step before it. Strip quoted history and signatures first, run per message on the new text only, and pass the sender as a structured input rather than asking the model to find it — reconstructing the thread from its headers is the prerequisite, not an optional refinement. If you take one thing from this page: the model should never be the thing that decides who sent a message.

When “you” resolves and when it does not

The second most common form is a request: “Can you get the contract over to legal before Thursday?” Now the owner is a recipient, and whether you can name them is arithmetic on the headers.

  • One address in To, none in Cc. “you” is that address. This is a safe resolution and it covers a large share of real threads.
  • A vocative in the text. “Priya — can you take this one?” The name is in the sentence and matches a recipient. Also safe, and it overrides the recipient count.
  • Several addresses in To. Unresolvable from the message alone. English does not distinguish singular and plural you, so “can you look at this” to four people is either one task with an unknown owner or four tasks. Do not pick the first recipient, do not pick the one whose name appears elsewhere in the thread, and do not pick the person who replies next, because that is a fact from the future and you would be writing it into the past.
  • A distribution list or a shared mailbox. “you” addressed to support@ is a team, which is a real owner kind and not a person. Keep it as a group rather than expanding it.

The important design decision is that the third case has an output. An action with a real task, a real deadline and an owner of null is more useful than one with an invented owner, because it can be routed to a human with a specific question: which of these four people is this for? A pipeline that always produces an owner cannot ask that question, and the errors it makes are invisible until somebody misses a deadline they never knew they had.

What counts as a commitment

The other half of the precision problem is that email is full of sentences shaped like commitments that are not. Three categories are worth encoding explicitly rather than leaving to judgement.

Hedged and collective

“We should probably update the runbook at some point” and “someone needs to chase the vendor” are observations. They have no owner and no date, and if you extract them you will bury the real actions under a drift of good intentions. A useful rule: a commitment needs a determinable actor, otherwise it is a suggestion, and suggestions get their own record type or none.

Conditional

“Happy to run the migration if we get sign-off from security” is a commitment with a precondition. Dropping the condition converts a contingent offer into a firm promise, which is how automated trackers acquire a reputation for nagging people about work they never agreed to do. Give the schema a nullable condition_text and fill it from “if”, “once”, “assuming”, “subject to”, “provided”.

Delegation

“I’ll ask Marco to sort out the DNS” contains exactly one commitment, and it belongs to the sender: the task is asking Marco. Marco has committed to nothing; he may not be on the thread. Attributing the DNS work to Marco is the single most common over-extraction in this problem and it is worth an explicit instruction, because the sentence reads as though it is about Marco.

Deadlines are relative to a header

Almost every deadline in email is relative: “Friday”, “end of day”, “next week”, “before the board meeting”. The anchor is the Date header of the message that contains it, and the anchor is not optional — the same sentence extracted from a message in June and one in September means different dates.

Two things go wrong here reliably. The first is that Date carries the sender’s offset, so “end of day” from a colleague in Singapore is a different instant from the same phrase sent from Lisbon, and a naive resolver that normalises everything to the server’s zone will move deadlines by up to a day. The second is that a bare weekday is ambiguous between this week and next: “Friday” sent on a Friday afternoon almost always means the following Friday, and sent on a Monday almost always means three days later. Encode a rule, document it, and keep the original words in a due_text field so a human can overrule it.

Resolve to a date plus a stated timezone plus the original phrase, never to a bare timestamp. The phrase is what a person will recognise; the timestamp is what you sort on; the zone is what makes them agree.

Later messages cancel earlier ones

A thread is a sequence of edits to a shared state, and extraction that treats each message independently produces a list of every commitment ever made, including the ones that were retracted four messages later. “Actually, Marco is picking this up” and “we agreed to drop this until Q4” are as much a part of the record as the original promise.

Handle this as a second pass over the thread in graph order rather than inside the per-message extraction, which cannot see the future. For each new message, ask whether it changes the status of any previously extracted action, and record the change as its own event with its own evidence span. Never rewrite the original record — the fact that a commitment was made and then withdrawn is exactly the thing somebody will want to see. This is also where thread order matters more than anywhere else, because a branch of the tree that a participant never saw cannot cancel anything.

A schema with an unresolved state

{
  "actions": [
    {
      "message_id": "[email protected]",
      "task": "Send the revised Q3 figures",
      "owner_email": "[email protected]",
      "owner_kind": "person",
      "owner_resolution": "from_header",
      "requested_by": null,
      "due_text": "Friday",
      "due_date": "2026-07-17",
      "due_zone": "+01:00",
      "condition_text": null,
      "status": "open",
      "evidence": "I'll send the revised figures on Friday."
    },
    {
      "message_id": "[email protected]",
      "task": "Get the signed contract to legal",
      "owner_email": null,
      "owner_kind": "unresolved",
      "owner_resolution": "ambiguous_you_multiple_recipients",
      "requested_by": "[email protected]",
      "due_text": "before Thursday",
      "due_date": "2026-07-16",
      "due_zone": "+01:00",
      "condition_text": null,
      "status": "needs_review",
      "evidence": "Can you get the contract over to legal before Thursday?"
    }
  ]
}

owner_resolution is the field that makes this auditable. A closed enum — from_header, sole_recipient, vocative, named_in_text, group_address, ambiguous_you_multiple_recipients, none — lets you count how often each path fires and sample the ones you trust least. If from_header is producing ninety per cent of your owners, the extraction is working. If named_in_text is, the model is reading names out of quoted history and you have a stripping bug.

Route everything with owner_kind of unresolved to review rather than to a tracker, and treat the rate as a health metric: it should be neither zero nor a third. Choosing a sampling rate for the rest is a separate decision, and the same commitments extracted from a set of minutes arrive already owned, which is why the two pipelines should not share a prompt.