Resources / Field guide

Building an Outlook add-in that saves a whole email thread

"Save this conversation to our system" sounds trivial until you discover Office.js can only see the one email that's open. Saving the whole back-and-forth means reaching into Microsoft Graph — which pulls in SSO, the On-Behalf-Of flow, and a multi-tenant app registration. Here's the architecture, end to end.

The wall you hit

Office.js is deliberately sandboxed: it exposes the active mail item and not much else. Your add-in can read the open email, including its conversationId — but it cannot enumerate the other messages in that conversation. There's no local API for "the rest of the thread." To get those messages you have to ask Microsoft's servers, and that means Microsoft Graph.

Why Graph, specifically

Graph has an endpoint that takes a conversationId and returns every message in that thread. Since Office.js already hands you the current message's conversationId, the plan writes itself: get a Graph token, call Graph with that ID, save each returned message as its own record. Without Graph there's simply no way to reach those other emails — the add-in can't see into Outlook's store, and Office.js won't expose them.

The token dance (this is the real work)

You can't call Graph directly from the add-in's browser context with the SSO token — that token is meant to be exchanged server-side. The correct flow has three hops:

  1. Add-in: request an SSO token, signaling you'll use Graph scopes:
    const ssoToken = await OfficeRuntime.auth.getAccessToken({
        forMSGraphAccess: true
    });
    // send ssoToken to your backend
  2. Your server: exchange the SSO token for a real Graph token using the On-Behalf-Of (OBO) flow. The SSO token can't call Graph itself; OBO trades it for one that can, scoped to the signed-in user.
  3. Your server → Graph: call the conversation endpoint with the Graph token and the conversationId, then save the messages.

That server-side OBO step is the part tutorials gloss over and the part that takes the most care to get right.

What you have to build

  • Manifest change: add the Graph permission your scenario needs (e.g. Mail.Read). Adding a scope means users re-consent.
  • SSO wired up: getAccessToken isn't on by default; enable Modern Authentication for the tenant and configure the add-in's app registration.
  • A backend Graph client: performs the OBO exchange and calls Graph. There's no way around having a server here.
  • A save-thread endpoint: takes a conversationId, fetches the thread, persists each message.
  • A guardrail: cap the number of messages (e.g. warn past 50) so a giant thread doesn't run away.

It's one integration, not one per customer. You register a single multi-tenant Azure AD app. Users from any Microsoft 365 organization can then use the add-in: the first time someone saves a thread, they see a one-time consent prompt ("This app wants permission to read your email — Allow?"), click once, and never see it again. Their IT admins don't need to configure anything per tenant. That's what makes this shippable as a product rather than a bespoke deployment.

Don't try to call Graph from the task pane directly. The SSO token from getAccessToken is not a Graph token and can't be used as one from a browser/SPA context — you must do the OBO exchange on your server. Trying to shortcut this is the most common dead end.

When this fits

This pattern suits any add-in that needs more than the open item — saving threads, syncing conversations into a CRM or case system, analyzing history. It's real work (SSO + OBO + a backend), so if you only ever need the single active email, stay in Office.js and skip Graph entirely. The moment "the whole conversation" enters the requirements, this is the road.

Building an Outlook or Microsoft 365 add-in?

SSO, Graph, On-Behalf-Of, multi-tenant consent — I've built the tricky parts and can build yours directly. Tell me what you need it to do.

Work with me