---
name: outlook-mail-ingest
description: "Operator-initiated Outlook mail ingestion. Activate when the operator asks to check, triage, or pull Outlook mail into memory ('check my Outlook', 'ingest these Outlook messages', 'pull that thread into the graph'). Drives a list, summarise, ask, ingest loop using the outlook-mail-list / outlook-mail-search and outlook-mail-ingest tools. Human-in-the-loop: nothing enters the graph without an explicit operator decision per message."
---

# Outlook mail ingestion

Invoked from `specialists:personal-assistant`.

Operator-initiated, human-in-the-loop ingestion of an Outlook mailbox into the business graph. Reviewed messages become `:ConversationArchive {source:'email'}` threads. A message enters the graph only when the operator says so, and only the ones they pick. There is no background poll, no automatic classifier, no silent writes.

This is the Microsoft Graph analogue of the IMAP `email-ingest` skill. It differs in one structural way: Outlook reads are stateless Graph calls, so there is no staged batch and no high-water mark. You list with the ordinary read tools and pass the exact message ids you want to `outlook-mail-ingest`.

## When to activate

Activate when the operator wants to bring Outlook mail into the agent's memory. Typical phrasings:

- "Check my Outlook."
- "Ingest these Outlook messages."
- "Pull that Outlook thread into the graph."

Do not activate for:

- Reading or searching mail already in the mailbox or the graph. Those are `outlook-mail-list`, `outlook-mail-search`, `outlook-mail-fetch-body`.
- Drafting or sending. Those are `outlook-draft`, `outlook-mail-reply`, `outlook-mail-send`.
- Setup, status, or OTP extraction. Those are direct tool calls.

## Flow

### 1. List

Call `outlook-mail-list` (or `outlook-mail-search` for a sender or subject filter). Both return a preview per message with its id, sender, subject, and `receivedDateTime`. They write nothing to the graph.

An account can hold more than one mailbox. When it does, every operational tool refuses unless it names one, and the refusal lists the connected mailboxes. That refusal means "say which mailbox", not "there is no mail". Resolve it with `outlook-mailbox-list`, then pass `mailbox` and carry on.

Reads page beyond the newest window. When a response carries a `nextCursor`, older messages remain behind it; pass the cursor back to page on. Absence of a `nextCursor` is the only thing that means you saw everything in range.

### 2. Summarise

Present the listed messages to the operator as a numbered or per-id summary. For each, surface the sender, the subject, and a one-line gist from the preview. Where a message is obviously one or the other, suggest a disposition ("newsletter, looks like discard"; "thread reply from a known correspondent, looks like ingest"). The suggestion is a hint, never an action; the operator decides.

There is no screener. If a message looks like phishing, say so in plain English so the operator can discard it by eye.

### 3. Ask

Ask the operator to confirm a disposition for every message you intend to ingest. Either a blanket call ("ingest all", "discard the rest") that you turn into the per-message map, or an explicit per-message choice ("ingest 1, 2, 4; discard 3"). If the operator answers ambiguously ("the work ones"), restate the messages you think they mean and confirm before proceeding. Never guess.

### 4. Ingest

Call `outlook-mail-ingest` with a `decisions` array: one `{ messageId, disposition }` per message, `disposition` being `ingest` or `discard`. The tool refuses an empty array and refuses a duplicate messageId, so pass a clean set with one disposition per id.

For each `ingest` message the tool fetches the full body and envelope, groups messages into threads by Graph `conversationId`, and resolves every From/To/Cc address closed-set against the graph. An address that does not already resolve to a `:Person` or `:AdminUser` for this account causes its whole thread to skip; the tool returns the unresolved addresses. It never creates a `:Person`.

On success the tool returns a one-line summary: how many were ingested, how many discarded, how many threads dispatched or skipped, and any unresolved addresses.

### 5. Confirm

Echo the tool's summary back to the operator in one sentence. If any thread was skipped for an unresolved address, tell the operator which address, and that adding that person to the graph and re-running will land the thread. If they want to draft a reply to anything just ingested, hand off to the Outlook compose tools.

## Edge cases

| Situation | Behaviour |
|-----------|-----------|
| The list returns zero messages | Tell the operator and stop. Do not call `outlook-mail-ingest`. |
| An address on a thread does not resolve | The thread is skipped and the address is returned. Surface it; the operator adds the person to the graph, then re-runs for that message. |
| Operator wants to ingest some now and review the rest later | Fine. Ids are explicit, so ingest exactly the ones decided now and list the rest again later. There is no high-water mark to advance and nothing is silently dropped. |
| Operator abandons mid-flow | Do not call `outlook-mail-ingest`. Nothing was staged and nothing changes. |
| A tool errors (Graph down, auth failure) | Surface the error verbatim. Do not retry silently. |

## Out of scope

- Automatic re-screening or classification. The operator is the screener.
- Scheduled or recurring ingestion. The operator initiates every cycle.
- Attachment ingest. This skill lands text threads only.
- Other channels. WhatsApp, IMAP email, and the public web have their own ingestion paths.

## Observability

Each ingest emits tagged lines the operator can grep:

- `[outlook-mcp] mail-ingest op=ingest id=<id> conversationId=<id> disposition=ingest` per approved message.
- `[outlook-mcp] mail-ingest op=thread-dispatched root=<key> messages=<n> participants=<n>` per landed thread.
- `[outlook-mcp] mail-ingest op=summary ingested=<n> discarded=<m> threadsDispatched=<n> threadsSkipped=<n> unresolved=<n>` once per call.

A complete cycle is greppable as the `[outlook-mcp] mail-ingest` lines for one session. A `threadsSkipped` above zero with an `unresolved` count is the closed-set resolution refusing an unknown participant, not a failure.
