Microsoft 365
A runbook for the m365 plugin. The agent gets a Microsoft 365 user of its
own and works in it the way a colleague does: it reads and answers its mail,
keeps its calendar, and finds, edits and files documents in OneDrive and
SharePoint. Everything goes through the Microsoft Graph API with an Entra ID
app registration.
Short version: a licensed user for the agent, an app registration with application permissions, an Exchange scope that fences the app into that one mailbox, the system activated in covey, two secrets, one line in ACCESS.md, three egress hosts. Section 2 walks through every click.
How it differs from its neighbours:
m365 |
email |
sharepoint |
|
|---|---|---|---|
| Exchange Online through Graph | any IMAP/SMTP server, password login | — | |
| Calendar | yes | — | — |
| Files | own OneDrive + every site the app may reach | — | one library behind a share link |
Exchange Online no longer accepts password logins over IMAP, so for a Microsoft
365 mailbox m365 is the way, not email.
1. What the agent can do
- Mail — list a folder (unread only, or by search words), read a message as text with its attachment list, put an attachment into the sandbox, reply or reply to all (with sandbox files attached), send a new mail, leave a draft for a person to check and send, move a mail into a folder, mark it read.
- Calendar — read the calendar between two times (series expanded), create an event with attendees or as a Teams meeting, accept / decline / tentatively accept an invitation, cancel an event it organises, look up colleagues' free/busy times.
- Files — find SharePoint sites, list a site's libraries, list and search a
library or its own OneDrive (
drive_idme), read a text file, download a file into the sandbox, upload one from it, write a text file, create folders. There is no delete action, on purpose. - Links —
open_linktakes a SharePoint, OneDrive or Teams-files link (a sharing link or the plain address of a file or folder) and answers the drive and path the file actions take. "Please add to the minutes here: " is a complete assignment. - Office documents — the plugin does not parse them. The agent downloads a
.docx,.xlsxor.pptx, edits it in the sandbox withpython3(docx,openpyxl,pptxare in the base workplace) and uploads it onto the same path.
Sending mail and inviting people reaches real people. The guard-rail subjects are
one per action (m365:send_mail, m365:create_event, …), so an organisation
that wants a human to look first puts require_approval on exactly those and
leaves reading free.
2. Setup
Two roles' work, in this order: first a Microsoft 365 admin (user, app, Exchange scope, SharePoint), then a covey admin (activate, secrets, egress, hire). Plan half an hour. Keep a scratch note open for five values you collect on the way:
| Value | From | Used in |
|---|---|---|
| Agent address | 2.1 | m365_url, Exchange scope |
| Directory (tenant) ID | 2.2 | m365_token |
| Application (client) ID | 2.2 | m365_token, Exchange, SharePoint |
| Client secret value | 2.2 | m365_token |
| Enterprise application object ID | 2.3 | New-ServicePrincipal |
2.1 A user for the agent (Microsoft 365 admin centre)
- admin.microsoft.com → Users → Active users → Add a user.
- Name it after its job (e.g. Office Agent), username e.g.
office@contoso.com. Assign a licence that includes Exchange Online and OneDrive (Microsoft 365 Business Basic is enough). - The password is never used by covey — set a long random one and do not hand it out. The agent signs in as the app, not as this user.
- Sign in once at outlook.office.com as this user
(or have the mailbox opened by an admin) and create the folders your playbook
names, e.g.
Erledigt. The plugin does not create mail folders.
A shared mailbox works for mail and calendar but has no OneDrive; use a licensed user when the agent should have files of its own.
2.2 The app registration (Entra ID)
entra.microsoft.com → Applications → App registrations → New registration. Name: e.g.
covey office agent. Supported account types: this organisational directory only. Redirect URI: leave empty. Register.On the overview page copy Application (client) ID and Directory (tenant) ID.
API permissions → Add a permission → Microsoft Graph → Application permissions (not Delegated). Add:
Permission For Mail.ReadWrite,Mail.Sendthe mailbox Calendars.ReadWritethe calendar Files.ReadWrite.AllOneDrive and document libraries Sites.ReadWrite.AllorSites.Selectedfinding sites — see 2.4 Then Grant admin consent for
. Every row must show a green Granted status; a permission without consent does nothing. Certificates & secrets → Client secrets → New client secret. Description
covey, expiry as your policy allows (at most 24 months). Copy the Value column right away — it is shown once. The Secret ID next to it is not the secret; using it givesinvalid_client(AADSTS7000215).Put the expiry date in a calendar. When it passes, every action fails with
invalid_client; renew under the same page and replacem365_tokenin covey.
2.3 Fence the app into the agent's mailbox (Exchange Online PowerShell)
Application permissions hold tenant-wide: after 2.2 the app could read and send from every mailbox in the organisation. Exchange's RBAC for Applications narrows mail and calendar to the one user. Do not skip this step.
Find the Enterprise application object ID: Entra ID → Applications → Enterprise applications → search the app's name → Object ID. This is a different ID from the one on the app registration page; the registration's object ID makes
New-ServicePrincipalfail or bind the wrong object.Open PowerShell (Windows PowerShell 5.1 or PowerShell 7) as an Exchange administrator:
Install-Module ExchangeOnlineManagement -Scope CurrentUser # once Connect-ExchangeOnline -UserPrincipalName admin@contoso.comCreate the scope, register the app with Exchange, and give it the three roles inside the scope:
New-ManagementScope -Name "covey-office" ` -RecipientRestrictionFilter "PrimarySmtpAddress -eq 'office@contoso.com'" New-ServicePrincipal -AppId <client-id> -ObjectId <enterprise-app-object-id> ` -DisplayName "covey office agent" New-ManagementRoleAssignment -App <client-id> -Role "Application Mail.ReadWrite" ` -CustomResourceScope "covey-office" New-ManagementRoleAssignment -App <client-id> -Role "Application Mail.Send" ` -CustomResourceScope "covey-office" New-ManagementRoleAssignment -App <client-id> -Role "Application Calendars.ReadWrite" ` -CustomResourceScope "covey-office"Back in Entra ID → the app registration → API permissions: remove
Mail.ReadWrite,Mail.SendandCalendars.ReadWritethere. Grants in Entra ID and in Exchange add up — as long as the tenant-wide ones stay, the scope narrows nothing.Check it:
Test-ServicePrincipalAuthorization -Identity <client-id> -Resource office@contoso.com Test-ServicePrincipalAuthorization -Identity <client-id> -Resource someone.else@contoso.comThe first answers
InScope : Truefor the three roles, the secondFalse. Changes can take up to two hours to apply in Exchange; a 403 right after this step is usually that delay.
The plugin itself only ever addresses the mailbox in m365_url. That is a
convention of the code, not a boundary: the boundary is this step.
2.4 SharePoint: which sites the agent may reach
Decide first where the agent will work, and especially where the links it is
sent will point. The agent opens a link as the app, not as its user: sharing a file
with office@contoso.com does not let the app in.
| Choice | Effect |
|---|---|
Sites.ReadWrite.All |
every site and every link in the tenant opens. Simplest; the app reaches the whole SharePoint estate |
Sites.Selected |
only sites granted to the app one by one. A link into any other site ends in a 403 that says so |
With Sites.Selected, grant each site. Either way below works; you need a
SharePoint administrator.
PnP PowerShell (shortest):
Install-Module PnP.PowerShell -Scope CurrentUser # once
Connect-PnPOnline -Url https://contoso.sharepoint.com/sites/Team -Interactive -ClientId <your PnP admin app>
Grant-PnPAzureADAppSitePermission -AppId <client-id> -DisplayName "covey office agent" `
-Site https://contoso.sharepoint.com/sites/Team -Permissions Write
Graph Explorer (aka.ms/ge, signed in as an admin who has
consented to Sites.FullControl.All there):
Site ID:
GET https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/Team→ copyid(three parts, comma-separated).Grant:
POST https://graph.microsoft.com/v1.0/sites/{site-id}/permissions { "roles": ["write"], "grantedToIdentities": [{ "application": { "id": "<client-id>", "displayName": "covey office agent" } }] }
Repeat per site. list_sites in covey then shows exactly those sites. The agent's
own OneDrive needs no grant — Files.ReadWrite.All covers it.
2.5 In covey
Activate the system for the organisation: Target systems → Microsoft 365 → Activate. A compiled plugin is off per organisation until then, and an agent's
ACCESS.mdline alone does not switch it on.Store the secrets at the agent: Agent → Settings → Secrets:
m365_url = office@contoso.com m365_token = <tenant-id>:<client-id>:<client-secret-value>For national clouds
m365_urltakes the endpoints after the address:office@contoso.us graph=https://graph.microsoft.us login=https://login.microsoftonline.us.Access — in the agent's
ACCESS.md:- system: m365 scope: read,writeEgress — Egress → Templates: adopt Microsoft 365 from the catalogue, then tick it on the agent page, tab Egress. Or add the hosts as the agent's own:
Host Why graph.microsoft.comthe API login.microsoftonline.comthe token *.sharepoint.comfile downloads: /contentredirects to the tenant's SharePoint host
2.6 Testing
- Connection test (Target systems → Microsoft 365 → Test connection): it
reads the inbox and answers
mailbox office@contoso.com reachable: 3 unread of 41 in the inbox. - Hire the agent, then send its address a short assignment from a colleague's
mailbox. Within the heartbeat interval a run starts; its recording shows
list_mail,get_mail, and the answer it sent. - Send it a SharePoint link in a second mail ("please add a line to this
document: ") to check
open_linkand the site grants.
2.7 When something fails
| Symptom | Cause | Fix |
|---|---|---|
entra id token: HTTP 401 … invalid_client / AADSTS7000215 |
wrong secret (the Secret ID was copied) or the secret expired | new client secret, copy its Value, replace m365_token (2.2) |
AADSTS700016 |
client ID does not exist in this tenant | check tenant-id:client-id order and values |
HTTP 403 … ErrorAccessDenied on mail or calendar |
role assignment missing, scope does not cover the address, or the change has not applied yet | run Test-ServicePrincipalAuthorization (2.3); wait up to two hours after a change |
HTTP 403 on every mailbox, also the agent's |
admin consent missing, or the Graph mail permissions were removed before the Exchange roles existed | 2.2 step 3, 2.3 step 3 |
HTTP 403 from open_link or list_drives |
Sites.Selected without a grant for that site |
grant the site (2.4) |
list_sites empty |
Sites.Selected with no site granted yet |
grant at least one site (2.4) |
listing works, download_file ends in Forbidden |
egress blocks the SharePoint download host | *.sharepoint.com (2.5 step 4); the egress log shows the refused CONNECT |
m365 is not activated, or the agent's prompt lists no Microsoft 365 actions |
system not activated for the organisation | 2.5 step 1 |
no m365_token stored for this agent |
secrets stored, but not at this agent | 2.5 step 2 |
3. Intake
There is no webhook: Graph change notifications need a publicly validated subscription that has to be renewed every few days. The intake is a heartbeat with a pre-check that costs one call:
- alle: 10m nur-wenn: m365 titel: Look after the mailbox
aufgabe: Read the unread mail (list_mail unread:true) and work on it by the playbook.
nur-wenn: m365 asks for the unread mail in the inbox (nur-wenn: m365:<folder>
for another folder). It reports one item per message (mail:<id>@<last modified>),
so it fires when a mail arrives or changes and stays quiet while the inbox stands
still, and a workflow on only_if: m365 runs one
case per mail. The agent's own mark_read, move_mail and reply_mail are
known to move the signature and do not wake it again.
Ids are Graph's immutable ids: a mail keeps its id when it is moved, so a note or a workflow case that names it stays true.
4. Limits
| What | Limit |
|---|---|
| Attachments on an outgoing mail | 3 MB together (Graph refuses a request over 4 MB) — a larger file goes to OneDrive with upload_file, and the mail carries its link |
| An attachment fetched into the sandbox | COVEY_M365_ATTACHMENT_MAX_MB, default 25 |
| An upload | COVEY_M365_UPLOAD_MAX_MB, default and maximum 250 (simple upload) |
read_file |
text up to 1 MB; anything else through download_file |
list_mail |
50 per call |