Microsoft 365 connector setup

Operator·8 minutes to read

This page is the setup half — creating the OAuth app Microsoft 365 needs in the Microsoft Entra admin center and getting its client id and secret into this deployment. Authorising a connection afterwards is the Authenticate control on the plugin sheet, which is what actually sends you to Microsoft.

This plugin bundles three connectors — mail, calendar and files — behind one app registration. One plugin, one consent, three connectors, and therefore one redirect URL, saying microsoft-365; there is none for outlook-mail, outlook-calendar or onedrive on their own.

1. Register the app in the Microsoft Entra admin center

  1. Open the Microsoft Entra admin center and register a new application under Identity → Applications → App registrations — this is your own registration, not Botseon's.

  2. Under Redirect URIs (platform Web), add the redirect URL from docs/connectors/README.md — with the quickstart's default BOTSEON_PUBLIC_URL, that is:

    http://127.0.0.1:3000/api/oauth/microsoft-365/callback
    
  3. Under Certificates & secrets, create a new client secret. The registration's Overview pane shows you the Application (client) ID; the secret's value is shown once — copy both now.

2. Scopes

Request the narrowest set that covers the tools this connector exposes (F-PLUG-1: minimum scopes per capability, read and write requested separately), as delegated permissions:

  • Mail.Read — read messages and folders; no writes.
  • Mail.ReadWrite — create, update and delete drafts and messages.
  • Mail.Send — send a message the member approved.
  • Calendars.Read — read calendars and events; no writes.
  • Calendars.ReadWrite — create, update and delete events.
  • Files.Read.All — read files and their metadata; no writes.
  • Files.ReadWrite.All — create and edit files.
  • User.Read — read the signed-in account's own profile, so a connection can be labelled with whose it is.
  • offline_access — issue a refresh token, so the connection survives past the access token's own short lifetime.

User.Read and offline_access are requested once, with the read half, because without them there is no refresh token and no account identity to label — the same grant covers all three connectors.

3. Enter the app

One command, with the client secret on standard input — there is no flag that takes it and no environment variable that holds it (see docs/connectors/README.md):

printf %s "$CLIENT_SECRET" | botseon oauth-app set \
  --org <organisation id> --slug microsoft-365 \
  --client-id <the client ID from step 1> \
  --auth-url https://login.microsoftonline.com/common/oauth2/v2.0/authorize \
  --token-url https://login.microsoftonline.com/common/oauth2/v2.0/token \
  --configured-by <your user id>

Both URLs are read from Microsoft's own current documentation of the v2.0 endpoints (Microsoft identity platform and OAuth 2.0 authorization code flow), not invented here; the common tenant covers both work/school and personal Microsoft accounts.

It stores the secret encrypted and prints the redirect URL from step 1 back, so you can check the two match. botseon oauth-app show --org <organisation id> --slug microsoft-365 says whether an app is configured and under which client id; nothing prints the secret back.

Never put either value in a bot's description, a memory, or anywhere else a model reads as ordinary text.

4. When a mailbox search is slow

A mailbox search is a full-text search over sender, subject and body, and Microsoft sometimes takes longer over it than over any other mail call. A bot waits up to 30 seconds for the search, however many pages of results it reads. If no answer comes in that time the run shows:

the provider did not answer the mailbox search within 30 seconds; try again, or search for something more specific

The connection is still authorised and nothing needs reconnecting: running the search again usually works, and a narrower search (a sender's address, an exact phrase from the subject) gives Microsoft less to look through. Reading a message, sending, replying, the calendar and OneDrive keep the usual 10-second limit.

5. What a bot can do in Outlook mail

All of these use the scopes in step 2; adding them needs no new consent.

ToolWhat it doesApproval card
outlook-mail.search, outlook-mail.get_message, outlook-mail.list_folders, outlook-mail.list_draftsRead mail, folders and the drafts in Draftsnone
outlook-mail.create_draftSave a draft with To, Cc, Bcc and an HTML or text body in Drafts. It is not sentnames the To line
outlook-mail.reply, outlook-mail.sendReply to the sender, or send a new messagenames the recipient
outlook-mail.send_draftSend a draft from Drafts — one the bot saved or one the member wrote in Outlook — with the AI disclosure written at its topnames every To, Cc and Bcc recipient
outlook-mail.reply_all, outlook-mail.forwardReply to everyone on a message, or forward itnames every recipient
outlook-mail.create_folderCreate a mail foldernames the folder
outlook-mail.move_messageMove a message into a foldernames the folder path
outlook-mail.apply_categoriesSet categories on a messagenames the message
outlook-mail.bulk_updateArchive, move into a folder, flag, unflag, or mark read or unread up to 100 messages at onceone card naming every message; the action and folder are in its details
outlook-mail.bulk_deleteDelete up to 100 messages at once; they go to Deleted Itemsone card listing every message; never always-allowed

A folder is named by its path, such as Clients/Larsen, or by one of Microsoft's well-known names (inbox, drafts, sentitems, deleteditems, archive, junkemail, outbox), which work whatever language the mailbox is in.

A reply to all is sent only to the people its card named. If Microsoft's reply-all would also reach someone the card left out, the reply is deleted unsent and the run stops with this message, naming who was missing:

reply-all would also reach <address>, who the approved list did not name; nothing was sent — read the message and list every recipient in to

A draft is sent only to the people its card named. The connector reads the draft again after the member allows the card — it may have been edited in Outlook meanwhile — and sends it only when it is still a draft and its To, Cc and Bcc are exactly the card's list. Otherwise the run stops with a message naming the difference, and the draft stays in Drafts untouched:

the draft would also reach <address>, who the approved list did not name; nothing was sent — read the draft again and list every recipient in to

The request shapes and the dates of the pages they were read from are in docs/research/connectors/outlook-mail-send-draft-2026-10-21.md. A sent draft goes to Sent Items, as when the member presses Send in Outlook. If runs stop on this message repeatedly for one member, the model is listing recipients from an old read: ask the bot to run outlook-mail.list_drafts first, which returns each draft's To, Cc and Bcc lines.

/send takes no body, so the AI disclosure rides in the tool's optional comment: the run always fills it, and once every recipient check has passed the connector writes it at the top of the draft (PATCH /me/messages/{id}, which needs Mail.ReadWrite, already in the write set) and then sends it. A refused draft is never edited. The nightly live check for this, and how to set up its test account, is docs/operations/connector-live-setup/microsoft-365.md.

A bulk call goes to Microsoft as one JSON batch per twenty messages (JSON batching); the request shapes and the dates of the pages they were read from are in docs/research/connectors/outlook-mail-bulk-actions-2026-10-21.md. A message Microsoft refuses inside a batch (gone, rate-limited) is listed in the bot's result under failed and the others are still changed; only when none of them changed does the run see an error. Delete is a move to deleteditems, as Outlook's own Delete button does, so a member can restore the messages; the connector never deletes permanently.

6. When a OneDrive upload goes wrong

onedrive.upload_file puts a file the bot made in its computer, such as a spreadsheet or a PDF, into a folder of the member's OneDrive. It uses the write scope from step 2 (Files.ReadWrite.All), so an app set up before it existed needs no new consent. Every upload waits for the member to approve its card, which names the file.

  • Size. Up to 16 MiB, the largest file a bot's computer hands back. A larger file is refused before anything is sent.
  • A name already in the folder. OneDrive keeps both: the new file gets OneDrive's renamed copy (budget 1.xlsx) and the bot's result says that name. The member's file is never overwritten.
  • Slow uploads. The connector waits up to 60 seconds. If OneDrive has not answered, the run shows OneDrive did not finish receiving the file in time. OneDrive may still have stored it, so look in the folder before asking the bot again.
  • OneDrive stored a different number of bytes than were sent or OneDrive stored a different file from the one sent. The size or the QuickXorHash OneDrive reported for the stored file did not match the bytes sent. Delete that copy in OneDrive and ask the bot again. If it happens twice, report it with the run id.

The request shapes and the dates of the Microsoft pages they come from are in docs/research/onedrive-folders-upload-2026-10-14.md and docs/research/connectors/onedrive-upload-2026-10-21.md. The nightly connector-live run checks an upload, and an upload to a name already taken, against a real OneDrive (extensions/connectors/microsoft-365/onedrive.live.test.ts, the Microsoft test account's secrets).

Last verified against build 445930e.