Zia.
Microsoft Office Add-insJune 18, 20266 min read

How to Build an Outlook Add-in with Office.js (Step-by-Step Guide)

A practical, code-included guide to building your first Outlook add-in with Office.js — scaffolding, the manifest, reading the current email, writing into a reply, and deploying.

How to Build an Outlook Add-in with Office.js (Step-by-Step Guide)

Outlook add-ins are web apps that run inside Outlook — in the reading pane, the compose window, or silently on send. Because they're built on Office.js and standard web tech (HTML, CSS, TypeScript), a single add-in runs across Outlook on Windows, Mac, the web, and the new Outlook client. No VSTO, no COM, no per-machine installers.

This guide walks through building a working Outlook add-in from scratch: scaffolding the project, understanding the manifest, reading the open email, writing text into a reply, and getting it in front of real users. Every snippet here is code I use in production client work.

What you'll need

  • Node.js 18+ and npm.
  • A Microsoft 365 account you can sideload add-ins into (a free developer tenant works).
  • Outlook — desktop or web. Use "new" Outlook or Outlook on the web for the fastest reload loop.
  • Basic familiarity with TypeScript and the terminal.

You don't need Visual Studio or any Microsoft-specific IDE. VS Code is plenty.

Step 1: Scaffold the project with Yeoman

Microsoft maintains a Yeoman generator that wires up the manifest, a dev server with HTTPS, and a sideloading workflow. Install it and generate an Outlook project:

npm install -g yo generator-office
yo office

Answer the prompts like this:

  • Project type: Office Add-in Task Pane project
  • Script type: TypeScript
  • Name: outlook-context-pane
  • Office host: Outlook

The generator produces a project roughly like this:

outlook-context-pane/
├── manifest.xml          # tells Outlook what the add-in is and needs
├── package.json
├── webpack.config.js
└── src/
    ├── taskpane/
    │   ├── taskpane.html
    │   └── taskpane.ts    # your add-in logic lives here
    └── commands/
        └── commands.ts

Step 2: Understand the manifest

The manifest is the contract between Outlook and your add-in. It declares the add-in's name, the URL it loads from, where it appears in the UI, and — importantly — what permissions it needs.

The single most common source of "why won't my add-in load" pain is the permission level. For an add-in that reads and writes mail, you need ReadWriteMailbox:

<Permissions>ReadWriteMailbox</Permissions>

The four mailbox permission levels, from least to most access, are Restricted, ReadItem, ReadWriteItem, and ReadWriteMailbox. Ask for the least you need — enterprise admins scrutinize this before approving a rollout.

Tip: If you only ever read the currently open message and never call Microsoft Graph, ReadItem is enough and makes admin approval far easier.

Step 3: Initialize Office.js

Office.js loads asynchronously. Nothing in the Office object model is safe to touch until the host signals it's ready. Every add-in starts by waiting on Office.onReady:

Office.onReady((info) => {
  if (info.host === Office.HostType.Outlook) {
    document.getElementById("run")?.addEventListener("click", run);
  }
});

info.host lets the same codebase behave differently in Outlook vs. Word or Excel if you ever share code across hosts — a common pattern in broader Microsoft 365 development.

Step 4: Read the current email

The open message (or the message being composed) is exposed as Office.context.mailbox.item. Simple properties like the subject and sender are available synchronously in read mode:

function run() {
  const item = Office.context.mailbox.item;
  if (!item) return;

  const subject = item.subject;
  const from = item.from; // { displayName, emailAddress }

  render(`
    <strong>${from?.displayName}</strong>
    &lt;${from?.emailAddress}&gt;<br />
    <em>${subject}</em>
  `);
}

The body is different — it can be large, so it's always fetched asynchronously. Use getAsync and specify whether you want plain text or HTML:

item.body.getAsync(Office.CoercionType.Text, (result) => {
  if (result.status === Office.AsyncResultStatus.Succeeded) {
    const text = result.value;
    console.log("First 200 chars:", text.slice(0, 200));
  }
});

This read-the-open-email pattern is the foundation of most Outlook add-in work — showing CRM context, ticket history, or AI-drafted replies alongside the message.

Step 5: Write into a reply

Reading is half the job; the value usually comes from acting. To insert content into a reply, use displayReplyForm or, when the add-in is already in a compose window, item.body.setSelectedDataAsync.

Here's a compose-mode example that drops a templated snippet at the cursor:

function insertReply() {
  const item = Office.context.mailbox.item;
  const snippet =
    "<p>Thanks for reaching out — I'll get back to you within one business day.</p>";

  item?.body.setSelectedDataAsync(
    snippet,
    { coercionType: Office.CoercionType.Html },
    (result) => {
      if (result.status !== Office.AsyncResultStatus.Succeeded) {
        console.error(result.error.message);
      }
    },
  );
}

Notice the shape every Office.js async call shares: a payload, an options object, and a callback that receives an AsyncResult with a status. Once you internalize that pattern, the whole API becomes predictable.

Step 6: Run and sideload

Start the dev server, which also sideloads the add-in into Outlook:

npm start

The first run installs a local HTTPS dev certificate and opens Outlook with your add-in loaded. Open any email and click your button in the ribbon — the task pane appears. Edit taskpane.ts, save, and reload the pane to see changes.

If sideloading misbehaves on desktop, switch to Outlook on the web: run npm run start:web (or sideload the manifest manually via Get Add-ins → My add-ins → Add a custom add-in). The web client has the fastest iteration loop.

Step 7: Event-based activation (no pane required)

The most powerful Outlook add-ins run without the user opening anything — for example, appending a disclaimer or checking for a missing attachment the moment someone hits Send. That's event-based activation, declared in the manifest and wired to a handler:

function onMessageSendHandler(event: Office.AddinCommands.Event) {
  const item = Office.context.mailbox.item;
  item?.body.getAsync(Office.CoercionType.Text, (result) => {
    const missingAttachment =
      /see attach/i.test(result.value) && item.attachments.length === 0;

    event.completed({
      allowEvent: !missingAttachment,
      errorMessage: missingAttachment
        ? "You mentioned an attachment but didn't attach one."
        : undefined,
    });
  });
}

Calling event.completed({ allowEvent: false }) actually blocks the send — a small feature that saves users from real mistakes.

Step 8: Deploy to real users

You have two paths:

  1. AppSource — for a public add-in. You submit the manifest, pass Microsoft's validation, and it becomes installable by anyone.
  2. Centralized Deployment — for internal tools. An admin uploads the manifest in the Microsoft 365 admin center and the add-in appears for chosen users or the whole tenant automatically, with no per-user install.

For business add-ins, centralized deployment is almost always what you want. Host the static web files anywhere HTTPS is served — Azure Static Web Apps is the natural fit — and point the manifest's source URLs at that host.

Common pitfalls

  • Add-in won't appear: the manifest URL isn't reachable over HTTPS, or the permission level is wrong. Check the browser console in the task pane (right-click → Inspect on desktop).
  • item is null: you touched the Office object before Office.onReady resolved, or no message is selected.
  • Changes don't show: clear the Office cache and re-sideload; desktop Outlook caches aggressively.
  • Works on web, not desktop: an API you're using isn't supported on that Outlook version — guard with Office.context.requirements.isSetSupported("Mailbox", "1.x").

Where to go next

From here, the natural next step is calling Microsoft Graph to pull mail, calendar, and contact data beyond the current item — that's how you build a real CRM or helpdesk pane. I cover that in the Microsoft Graph API guide.

If you'd rather have a production-ready add-in built and shipped for you — manifest, SSO, Graph integration, and AppSource submission included — that's exactly the Outlook add-in development work I do for clients.

Related services

Need this built for you? Here's where I can help.

Zia ul Qamar

Written by

Zia ul Qamar

Full-stack developer specializing in React, Next.js, and Node.js, with a rare focus on Microsoft Office & Google Workspace add-ins. I build production tools and automations for international clients.