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
-
Open the Microsoft Entra admin center and register a new application under Identity → Applications → App registrations — this is your own registration, not Botseon's.
-
Under Redirect URIs (platform Web), add the redirect URL from
docs/connectors/README.md— with the quickstart's defaultBOTSEON_PUBLIC_URL, that is:http://127.0.0.1:3000/api/oauth/microsoft-365/callback -
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.
| Tool | What it does | Approval card |
|---|---|---|
outlook-mail.search, outlook-mail.get_message, outlook-mail.list_folders, outlook-mail.list_drafts | Read mail, folders and the drafts in Drafts | none |
outlook-mail.create_draft | Save a draft with To, Cc, Bcc and an HTML or text body in Drafts. It is not sent | names the To line |
outlook-mail.reply, outlook-mail.send | Reply to the sender, or send a new message | names the recipient |
outlook-mail.send_draft | Send a draft from Drafts — one the bot saved or one the member wrote in Outlook — with the AI disclosure written at its top | names every To, Cc and Bcc recipient |
outlook-mail.reply_all, outlook-mail.forward | Reply to everyone on a message, or forward it | names every recipient |
outlook-mail.create_folder | Create a mail folder | names the folder |
outlook-mail.move_message | Move a message into a folder | names the folder path |
outlook-mail.apply_categories | Set categories on a message | names the message |
outlook-mail.bulk_update | Archive, move into a folder, flag, unflag, or mark read or unread up to 100 messages at once | one card naming every message; the action and folder are in its details |
outlook-mail.bulk_delete | Delete up to 100 messages at once; they go to Deleted Items | one 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 sentorOneDrive 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.