Zia.
Zia Ul QamarMicrosoft 365 & Office Add-in DeveloperAboutGitHubLinkedInX
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 an email, and writing a reply.

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 are built on Office.js and standard web technology, one codebase can target supported Outlook clients on Windows, Mac, the web, and new Outlook. Available features depend on the client's Mailbox requirement sets; unlike VSTO or COM, the web add-in itself does not require a per-machine plugin installation.

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 part of 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.