Why My Outlook Add-in Lies About Its Version (And Yours Should Too)

May 12, 2026 | 6 min read

I recently built an Outlook add-in. It archives emails and attachments to SharePoint, lets you attach cloud files while composing, the works. It ran great on desktop, great in Outlook on the web. Then I opened Outlook on my phone to test it there.

The add-in was gone.

Not broken. Not throwing errors. Just… not there. No button, no task pane, nothing. Outlook mobile acted like my add-in didn’t exist.

TL;DR

Your manifest’s MinVersion is a filter, not documentation. If you declare the requirement set your best feature needs, every host that doesn’t support that version will hide your add-in completely. Declare the lowest version your minimum viable experience needs instead (for my add-in that’s Mailbox 1.5), then use Office.context.requirements.isSetSupported() in your code to unlock the newer stuff where it’s actually available.

The Mistake That Feels Like the Right Thing

Here’s the trap. My add-in uses getAsFileAsync() to grab the full email as a file before uploading it to SharePoint. That API lives in the Mailbox 1.14 requirement set. So the obvious, honest, “do the right thing” move is to declare that in the manifest:

<Requirements>
  <Sets>
    <Set Name="Mailbox" MinVersion="1.14" />
  </Sets>
</Requirements>

Makes sense, right? The manifest should declare what the code needs. That’s what a manifest is for.

Wrong.

Outlook reads that MinVersion and treats it as a door policy: “You need Mailbox 1.14 to see this add-in at all.” Outlook on iOS and Android supports Mailbox 1.5 (plus a handful of newer APIs cherry-picked on top). 1.5 is less than 1.14, so mobile doesn’t fail gracefully, it doesn’t show a warning, it just never lets your add-in through the door. That’s why my add-in vanished on my phone.

The manifest version isn’t describing your code. It’s deciding who gets to see your add-in.

The Fix: Degrade the Manifest, Upgrade at Runtime

The fix felt dirty the first time I did it: I “degraded” the manifest down to Mailbox 1.5, even though my code is full of 1.13 and 1.14 APIs.

This is what my add-in actually ships with:

<Requirements>
  <Sets>
    <Set Name="Mailbox" MinVersion="1.5" />
  </Sets>
</Requirements>

And the same low floor inside VersionOverrides:

<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides/1.1" xsi:type="VersionOverridesV1_1">
  <Requirements>
    <bt:Sets DefaultMinVersion="1.5">
      <bt:Set Name="Mailbox"/>
    </bt:Sets>
  </Requirements>
  ...
</VersionOverrides>

Why 1.5? Because that’s what the minimum viable version of my add-in needs: open a task pane, read the current message. Everything above that is a bonus feature, and bonus features shouldn’t decide whether the add-in exists.

One more thing mobile needs, and this one is easy to miss: mobile only renders what you explicitly declare in a MobileFormFactor. Desktop extension points don’t carry over.

<MobileFormFactor>
  <ExtensionPoint xsi:type="MobileMessageReadCommandSurface">
    <Group id="mobileReadGroup">
      <Label resid="groupLabel" />
      <Control xsi:type="MobileButton" id="mobileReadOpenPaneButton">
        <Label resid="taskPaneButtonLabel" />
        <Icon xsi:type="bt:MobileIconList">
          <bt:Image size="25" scale="1" resid="icon25" />
          <!-- ...more sizes and scales... -->
        </Icon>
        <Action xsi:type="ShowTaskpane">
          <SourceLocation resid="messageReadTaskPaneUrl" />
        </Action>
      </Control>
    </Group>
  </ExtensionPoint>
</MobileFormFactor>

Low floor + MobileFormFactor, and suddenly the add-in shows up on my phone. 🎉

Now Your Code Has to Keep the Promise

Here’s the deal you just made: the manifest promises Outlook almost nothing, so your code can’t assume anything. Every API newer than your floor needs a runtime check before you touch it.

Office.js has exactly the tool for this: isSetSupported(). I keep all of mine in one file, platformDetection.ts:

export function isMobilePlatform(): boolean {
  if (typeof Office === 'undefined' || !Office.context?.diagnostics?.platform) {
    return false;
  }

  const hostInfo = Office.context.diagnostics.platform;
  return hostInfo === Office.PlatformType.iOS ||
         hostInfo === Office.PlatformType.Android ||
         hostInfo === Office.PlatformType.Universal; // Includes mobile web
}

export function supportsArchive(): boolean {
  if (typeof Office === 'undefined' || !Office.context?.requirements) {
    return false;
  }

  // getAsFileAsync lives in Mailbox 1.14
  return Office.context.requirements.isSetSupported("Mailbox", "1.14");
}

export function supportsMultiSelect(): boolean {
  if (typeof Office === 'undefined' || !Office.context?.requirements) {
    return false;
  }

  return Office.context.requirements.isSetSupported("Mailbox", "1.13");
}

What’s happening here?

  1. isMobilePlatform() asks the host directly what platform it’s running on via Office.context.diagnostics.platform. Note that Universal covers mobile web too, that one surprised me.
  2. supportsArchive() checks for Mailbox 1.14 before the code anywhere near getAsFileAsync() runs. On mobile this returns false, and the UI adapts instead of crashing.
  3. supportsMultiSelect() does the same for multi-select (Mailbox 1.13). Desktop users get “archive 20 emails at once”, mobile users get one at a time, and nobody gets an error.
  4. Every function starts with a typeof Office === 'undefined' guard, so the same code doesn’t explode if it runs outside Outlook (looking at you, local dev in a plain browser tab).

Then the components just ask:

if (supportsArchive()) {
  // Full experience: grab the email with getAsFileAsync and upload it
} else {
  // Degraded experience: hide the feature, or take another route
}

The pattern scales nicely too. In my add-in, when getAsFileAsync isn’t available on mobile, I don’t hide the archive button, I fall back to fetching the message through Microsoft Graph instead. I wrote a follow-up post about exactly that: Same Email, Different Source: Falling Back to Microsoft Graph on Outlook Mobile.

The Contract (Write This on a Sticky Note)

Manifest floor = the lowest host you still want to appear in. Requirement sets in code = runtime capability checks, not install-time gates.

The manifest decides where you exist. The code decides what you can do once you’re there. Mixing those two up is how add-ins silently disappear from half your users’ devices.

Gotchas I Hit Along the Way

  • Your manifest can quietly use newer sets than it declares. My manifest declares 1.5 but wires up an OnMessageCompose launch event, which needs Mailbox 1.12. That’s fine, hosts simply ignore extension points they don’t understand. But it means the manifest validator won’t save you here; you have to know which parts light up where.
  • Mobile only gets what MobileFormFactor explicitly exposes. No MobileFormFactor, no mobile button, even with a perfect 1.5 floor. And as of writing, mobile read mode is basically the only surface you get.
  • Test on an actual phone. The desktop “mobile preview” and your imagination will both lie to you. Sideload it, open a real email on a real device, and watch what happens.

Wrapping Up

Declaring MinVersion="1.14" because your code uses a 1.14 API feels correct, and it’s exactly how you make your add-in invisible on mobile. Degrade the manifest to the lowest version your core experience needs, add a MobileFormFactor, and let isSetSupported() sort out the rest at runtime.

It felt like lying to the manifest at first. It’s not, it’s just answering the question the manifest is actually asking: “What’s the least you need to be useful?”

References 📚

Jeppe Spanggaard

Jeppe Spanggaard

A passionate software developer. I love to build software that makes a difference!