Recipe: connect Microsoft 365, Outlook drafts and Teams messages
This recipe covers three things that sign in with your own Microsoft Entra app:
- the
microsoft_365connection preset, so a board can read recent Outlook mail (no bodies), read and write Excel cells, list OneDrive files and read your Outlook calendar while it works; - the
outlook_draftdestination, which puts an approved email in your Outlook Drafts and never sends it; - the
teams_channel_messagedestination, which posts the approved text to one Teams channel after a person approves the card.
Read Connect a service that signs in with OAuth first for how OAuth connections, limits, requests and receipts work.
What BakedBrie never does with Microsoft:
- It never sends email. The connection has no mail write at all: BakedBrie refuses Outlook send, reply, forward, draft and every other mail write on every connection. The Outlook draft destination asks Microsoft only for
Mail.ReadWrite, neverMail.Send, so its sign-in cannot send mail even by mistake. - It never posts to Teams or chats, or sends invites, through a connection. Every Teams post, chat message and Outlook calendar event write is refused. The only way a message reaches Teams is the Teams channel message destination, after a person approves that exact text.
- Nothing wider than you tick. The connection asks for
offline_accessplus exactly the permissions of the operations you choose, computed for you. If Microsoft grants more than BakedBrie asked for (other thanUser.Read,openid,profileandemail, which Microsoft may list because the app was granted them before), the sign-in is refused and removed (OAUTH_SCOPE_WIDER_THAN_REQUESTED).
Before you run it: register one app
- In the Microsoft Entra admin center, open App registrations, New registration. Choose Single tenant only (Microsoft recommends it for most apps). BakedBrie signs in at your own tenant's endpoint, never at
common. - Under Authentication, add a Web redirect URI, character for character (the same for every BakedBrie connection and destination):
https://app.bakedbrie.com/settings/destinations/oauth/callback
- Under Certificates & secrets, make a New client secret. It lasts at most 24 months (Microsoft suggests less than 12). Copy the value at once: Microsoft never shows it again. Put it in your own shell, for example
export MICROSOFT_CLIENT_SECRET=..., and never paste it into the chat. Type its expiry date as "Secret expires on" on the connection (client_secret_expires_on), and BakedBrie reminds you 14 days before it ends.- Microsoft now recommends a certificate instead of a client secret for production apps, and a tenant's app policy may block secrets. BakedBrie supports only a client secret for now.
- Under API permissions, add Microsoft Graph, Delegated permissions, only for what you will use (table below), plus
offline_access. - Copy the Directory (tenant) ID and the Application (client) ID from the app's Overview page. The tenant id is typed before sign-in (below). Keep it out of shared chats; it is not a secret, but it names your organization.
- Consent. By default, people can consent to permissions that do not need an admin. If your tenant turned user consent off, limits it to permissions an admin classified as low impact (and these are not in that list), or the app requires user assignment, an admin clicks Grant admin consent once on the API permissions page. None of the permissions below needs admin consent in Microsoft's permissions reference.
whoamimust showconnectionsandconnections_oauthon (anddestinations,custom_oauthandapi_workflowfor the two destinations).
The connection: operations and permissions
Tick only what the board needs. The create form ticks the three reads marked "default"; writes are never ticked for you. offline_access is always added, so BakedBrie gets a refresh token.
| Operation | What it does | Reads or writes | Permission |
|---|---|---|---|
outlook_list_mail | Up to 25 messages received since a date, newest first: subject, sender name and address, date, read and attachment flags. No message bodies. | read | Mail.ReadBasic |
excel_read_range | Up to 50 rows of a range in one workbook on OneDrive or SharePoint, first 8 columns | read | Files.ReadWrite |
excel_write_range | Writes one value into one cell; never a formula | write | Files.ReadWrite |
onedrive_list (default) | Up to 50 files and folders at the top of your OneDrive | read | Files.Read |
onedrive_get (default) | One file's name, size and date by its item id | read | Files.Read |
calendar_list_events (default) | Up to 50 events between two dates (UTC): subject, times, canceled. No bodies or attendees | read | Calendars.ReadBasic |
Notes:
- **Excel needs
Files.ReadWriteeven to read.** Microsoft lists no lower permission for reading a range, soexcel_read_rangeis not ticked by default. The workbook item id, the worksheet name (no spaces) and a range likeA1:H50are the inputs. - Excel writes go to one cell, like
B7. BakedBrie refuses a value that starts with=,+,-or@, so it can never become a formula. - Item ids are letters, digits,
-and_, as OneDrive for Business uses. A single-tenant app does not sign in personal Microsoft accounts. - Times are UTC: date inputs become
T00:00:00Z, and Microsoft answers times without an offset in UTC.
What the board sees, and the taint rule
Answers are provider data, shown to agents only as untrusted facts. Anything a person typed is free_text: mail subjects and sender names, Excel cells, file names and event subjects. A run that read a non-empty free_text field can only read in the same round (CONNECTION_WRITE_TAINTED), so "read the sheet, then write a cell" in one agent round needs a person. A sender address is kept as an email value, but it can never address a draft: draft recipients come only from a customer or contact read (QuickBooks, Xero or HubSpot).
excel_write_range runs within limits a person sets in the web app, and BakedBrie sends it at most once. Microsoft documents no idempotency key for it, so if BakedBrie cannot tell whether Microsoft accepted a write, it never sends it again by itself: check the workbook before a person chooses Send again.
The prompt: the connection
In BakedBrie, connect Microsoft 365 to the board "{{BOARD_NAME}}". Use only the BakedBrie MCP tools. Start with whoami and stop if connections or connections_oauth is off. First read /docs/recipes/connections-oauth and /docs/recipes/microsoft-365 with read_docs.
My app's client id is {{CLIENT_ID}}, its Directory (tenant) ID is {{TENANT_ID}}, and its client secret is in my environment variable MICROSOFT_CLIENT_SECRET, expiring on {{YYYY-MM-DD}}. The operations I want: {{OPERATIONS, for example onedrive_list, calendar_list_events}}.
1. Call manage_connection with action presets, preset microsoft_365 and operations [my operations]. Show me the computed scopes.
2. Call prepare_secret with kind microsoft, purpose connection and env_var MICROSOFT_CLIENT_SECRET. Run the command it returns in my shell exactly as given, then use the drop_id. Never print the secret.
3. Create it: manage_connection action create, name "Microsoft 365", preset microsoft_365, operations [my operations], client_id {{CLIENT_ID}}, client_secret_expires_on {{YYYY-MM-DD}}, credential {"drop_id": "<the drop id>"}, and a new request_id.
4. Call manage_connection action connect_oauth with typed_values {"tenant": "{{TENANT_ID}}"}. Give me the link it returns: only a person can finish signing in, and the link works once, for 10 minutes. I open it in the browser where I am signed in to BakedBrie, approve at Microsoft, and click Finish connecting. Then call oauth_status until the grant is connected, or tell me its error code.
5. Attach it: manage_board_connection action attach, handle microsoft, the operations, the agents and stages that may use it, and when_to_use.
6. Stop and tell me: "Only a person can let a board use a connection. Open the limits link and set limits." Suggest $0.00 per call, reads 10 per card and 200 per month, writes 1 per card and 20 per month.
The tenant id must be the uuid from the Overview page. BakedBrie lowercases it and puts it only into the Microsoft sign-in path, https://login.microsoftonline.com/<tenant>/oauth2/v2.0/authorize and .../token; it never changes the host. common, organizations, a domain name or anything else is refused before any link is made (OAUTH_TYPED_VALUE_INVALID). Changing the tenant means signing in again.
Outlook drafts
The outlook_draft destination works like the Gmail draft in Draft invoice reminders in Gmail: after a person approves the card, BakedBrie makes exactly one draft in the signed-in person's Outlook Drafts with POST https://graph.microsoft.com/v1.0/me/messages. Nothing is ever sent.
- Its sign-in asks for exactly
offline_access Mail.ReadWrite.Mail.ReadWritedoes not include permission to send mail, and BakedBrie never asks forMail.Send, so the sign-in itself cannot send. - The card's result needs a
bakedbrie-emailblock. The draft has exactly one recipient, no cc, bcc or reply-to, and the recipient must be a customer or contact email that BakedBrie read on this card (a QuickBooks customer, a Xero contact or a HubSpot contact). Anything else is refused (EMAIL_RECIPIENT_UNVERIFIED). - Create it with
manage_destinationaction create,webhook{"preset": "outlook_draft", "settings": {"client_id": "<client id>", "tenant": "<tenant id>"}}and the client secret byprepare_secret(purposedestination). The token request goes to your tenant's path onlogin.microsoftonline.com. - The receipt shows the draft's message id. Open Outlook Drafts, check it, and send it yourself.
Teams channel messages
The teams_channel_message destination posts the approved result to one channel with POST https://graph.microsoft.com/v1.0/teams/<team id>/channels/<channel id>/messages, only after a person approves the card.
- Its sign-in asks for exactly
offline_access ChannelMessage.Send(no admin consent). The signed-in person must be able to post in that channel. Personal Microsoft accounts are not supported by Microsoft for this. - Finding the ids. In Teams, copy the channel's link. It looks like
https://teams.microsoft.com/l/channel/<channel id>/<channel name>?groupId=<group id>&tenantId=<tenant id>. The channel id is the part after/channel/, URL-decoded, like19:...@thread.tacv2. The team id is thegroupIdvalue: a team has the same id as its Microsoft 365 group. - Create it with
manage_destinationaction create,webhook{"preset": "teams_channel_message", "settings": {"client_id": "<client id>", "tenant": "<tenant id>", "team_id": "<group id>", "channel_id": "19:...@thread.tacv2"}}. Only a person in the web app sets or changes the team and channel. - The approved text is posted exactly as approved, as plain text, at most 20,000 bytes (well under Teams' limit of about 100 KB per post), with no mentions or attachments. A newer version that nobody approved is never posted. If BakedBrie cannot tell whether Teams accepted the post, it never posts again by itself.
Sign-in facts to know
- Access tokens usually last 60 to 90 minutes; BakedBrie uses the lifetime Microsoft sends and refreshes them itself.
- Refresh tokens last 90 days and Microsoft replaces them on every use; BakedBrie stores the new one each time.
- Microsoft has no revocation endpoint. Disconnecting deletes BakedBrie's copy of the sign-in and says so: "BakedBrie deleted its copy. To remove the app's access, open My Apps or ask your admin." Until you do, the refresh token stays valid at Microsoft for up to 90 days, although BakedBrie no longer holds it.
- Adding or removing an operation changes the permissions, so the connection asks you to sign in again with exactly the new set.
- Microsoft limits calls per app and tenant. When it answers 429, BakedBrie waits as long as Microsoft's
Retry-Aftersays. - Provider events (Graph change notifications) are not supported yet. Use a schedule.
Refusals you may see
| Code | What it means | Fix |
|---|---|---|
OAUTH_TYPED_VALUE_INVALID | The tenant is not a Directory (tenant) ID. | Copy the uuid from the app's Overview page. |
OAUTH_SCOPE_WIDER_THAN_REQUESTED | Microsoft granted a permission BakedBrie did not ask for. | Remove unused API permissions from your app registration, then sign in again. |
CONNECTION_SCOPE_WIDER_THAN_OPERATIONS | The definition asks for a permission no ticked operation needs. | Remove it, or tick the operation that needs it. |
CONNECTION_EMAIL_ENDPOINT_REFUSED | A connection path sends or writes mail. | Use the Outlook draft destination. |
CONNECTION_MESSAGE_ENDPOINT_REFUSED | A connection path posts to Teams or a chat, or writes a calendar event. | Use the Teams channel message destination. |
CLIENT_SECRET_EXPIRING | The client secret ends within 14 days. | Make a new secret in Certificates & secrets and update the connection. |