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.

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,
ReadItemis 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>
<${from?.emailAddress}><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:
- AppSource — for a public add-in. You submit the manifest, pass Microsoft's validation, and it becomes installable by anyone.
- 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).
itemis null: you touched the Office object beforeOffice.onReadyresolved, 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.

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.
Related posts

Microsoft Graph API: A Practical Guide for Office 365 Developers
A practical Microsoft Graph API guide for Office 365 developers — auth with MSAL, delegated vs application permissions, calling mail/calendar/users, pagination, and add-in SSO, with working code.
July 8, 2026

Getting Started with Google Apps Script: Automate Google Workspace
A hands-on introduction to Google Apps Script — write your first script, automate Google Sheets and Gmail, add custom menus, and run code on a schedule with triggers.
June 30, 2026