Most OpenClaw examples assume Gmail because Google publishes tidy quick-start guides. If your company standardised on Outlook and Microsoft 365, the docs leave gaps. This post closes them. We’ll register an Azure AD application, grab Microsoft Graph tokens, grant mail and calendar scopes, show the OpenClaw config that finally makes agent.sendEmail() hit Exchange Online—no IMAP hacks involved.

Why Microsoft 365 integration is slightly trickier than Gmail

Gmail exposes well-scoped OAuth flows that map 1-to-1 with Google APIs. Microsoft lumps everything—mail, calendar, Teams, OneDrive—into Graph. The good news: one token covers many services. The bad news: permission names are confusing, admin consent is often mandatory, and you’ll touch both Azure Portal and the Graph Explorer before the first email lands.

OpenClaw doesn’t abstract Graph yet (issue #4289). You drop credentials into the gateway UI or .env, and the agent calls Graph directly through the built-in graphMail helper. That keeps the surface area small but means initial Azure work is on you.

Prerequisites

  • Microsoft 365 Business or Enterprise tenant (E1+). Personal Outlook.com won’t work.
  • Azure AD admin role (or an admin willing to grant consent).
  • OpenClaw >= 1.13.0 running on Node 22+. The Graph helper landed in 1.13.
  • curl or gh for quick token tests.

Step 1 — Register an Azure AD application

  1. Open Azure Portal → Azure Active Directory → App registrations → New registration.
  2. Name it OpenClaw-Graph-Prod (friendly name; pick anything).
  3. Supported account type: Accounts in this organizational directory only.
  4. Redirect URI: choose Web and enter https://gateway.openclaw.app/oauth/callback (if you self-host change the domain).
  5. Click Register.

On success you’ll see Application (client) ID and Directory (tenant) ID. Copy both—OpenClaw needs them.

Create a client secret

  1. Navigate to Certificates & secretsNew client secret.
  2. Description: openclaw-prod, Expires: 24 months (shorter is better, automate rotation later).
  3. Save and copy the Value immediately. You will not see it again.

Step 2 — Grant Microsoft Graph API permissions

OpenClaw usually needs:

  • Mail.ReadWrite — read + send + move mail.
  • Mail.Send — specifically required for SMTP MSA restriction bypass.
  • Calendars.ReadWrite — optional, for scheduling tasks or parsing invites.
  • offline_access — so the refresh token survives after the initial login.

In Azure Portal hit API permissions → Add a permission → Microsoft Graph → Delegated permissions, search and tick the boxes above. Click Add permissions.

Because Mail.ReadWrite is sensitive, tenant-wide Admin consent is required. Still on API permissions hit Grant admin consent for <Tenant>. If you’re not an admin, send the generated link to someone who is.

Step 3 — Configure the OAuth callback in OpenClaw

If you use ClawCloud, skip: the gateway already handles the OAuth code exchange. For self-hosted gateways add this to config/gateway.json:

{ "auth": { "providers": { "m365": { "clientId": "<APPLICATION_ID>", "clientSecret": "<CLIENT_SECRET>", "tenantId": "<DIRECTORY_ID>", "redirectUri": "https://bot.example.com/oauth/callback" } } } }

Restart gateway:

$ npm run gateway

In the UI under Integrations → Microsoft 365 click Connect. You’ll be redirected to Microsoft login, see the permission list, accept, return.

Step 4 — Smoke test: reading the last 10 emails

Create a trivial tool script inside openclaw/tools/graphMail.js:

import { graphClient } from "@openclaw/plugins/m365"; export default async function listMail(ctx) { const client = await graphClient(ctx.auth.tokens.m365); const { value } = await client .api('/me/mailFolders/Inbox/messages') .top(10) .orderby('receivedDateTime DESC') .get(); return value.map(m => ({ id: m.id, subject: m.subject })); }

Run:

$ openclaw tool graphMail [ {"id":"AAMkAD…","subject":"Quarterly SOC2 files"}, … ]

If that prints subjects, your token works.

Step 5 — Sending mail from an agent flow

OpenClaw’s DSL exposes agent.sendEmail(to, subject, body). Under the hood it chooses Gmail or Graph depending on which token exists.

when "invoice uploaded" { const pdf = event.file; const htmlBody = `Invoice attached:

${pdf.link}`; await agent.sendEmail("ap@corp.example", "New vendor invoice", htmlBody, { attachments: [pdf] }); }

Microsoft flips if you forget the From header or try to spoof addresses outside the tenant. If the agent identity should be billing-bot@corp.example, create a shared mailbox and add the service principal Send as permission:

$ az ad app permission add --id \ --api 00000003-0000-0000-c000-000000000000 \ --api-permissions 5349a447-9bd8-4df2-8880-3b1c288d736c=Role

Then Grant admin consent again.

Step 6 — Calendar access and scheduling tricks

Same token, different endpoint.

const client = await graphClient(ctx.auth.tokens.m365); await client.api('/me/events') .post({ subject: 'One-on-one with CTO bot', start: { dateTime: '2024-07-11T14:00:00', timeZone: 'UTC' }, end: { dateTime: '2024-07-11T14:30:00', timeZone: 'UTC' } });

The Graph API auto-sends an invite email. Handy, but can spam if your agent creates many tentative holds; throttle with OpenClaw’s schedule helper:

schedule.every("weekday 07:00", async () => { const meetings = await agent.getCalendar({ days: 1 }); // summarise and post to Slack channel });

How this differs from Gmail integration

  • Scopes: Gmail uses https://mail.google.com/ (one mega-scope) and calendar. Microsoft splits but still broad. No least privilege nirvana either way.
  • Admin consent: only Microsoft forces a global admin click for Mail.ReadWrite. Google lets users self-grant.
  • Token lifetime: Google refresh tokens live until revoked. Microsoft’s default is 90 days inactivity unless you configure token lifetime policies.
  • Sending limits: Exchange Online caps at 10 K recipients/day, Gmail Workspace at 2 K internal + 2 K external. Know your ceilings before unleashing newsletter bots.
  • Transport fallback: Gmail allows raw SMTP with OAUTH2. Microsoft disabled smtp.office365.com modern auth for new tenants in 2023; Graph is required.

Bonus: hooking into Microsoft Teams

Teams messages also ride Graph. Add Chat.ReadWrite delegated permission. After consent:

const client = await graphClient(ctx.auth.tokens.m365); const chat = await client.api('/chats').filter("topic eq 'Finance Alerts'").get(); await client.api(`/chats/${chat.value[0].id}/messages`).post({ body: { content: 'Automated reminder: expense reports close Friday.' } });

The Teams UI shows messages from OpenClaw Bot. Branding comes from Expose an API → Application ID URI, then uploading a custom icon. No need to register a separate Teams app unless you want advanced tabs or meeting extensions.

Security & compliance notes

  • Rotate the client secret every semester; store it in Azure Key Vault or ClawCloud Secret Manager, not .env committed to Git.
  • Disable unused scopes; Graph tokens are bearer tokens—if leaked they grant everything inside scp.
  • Exchange Online auditing logs agent actions under ApplicationDisplayName. Make sure that name is unique so SecOps can trace.
  • PIM (Privileged Identity Management) works with app registrations; enforce approval for admin consent flows.

Next step: production hardening checklist

  1. Switch redirect URI to your company domain with HTTPS.
  2. Enable Conditional Access policy Require MFA for Azure portal— doesn’t break service principals.
  3. Write a GitHub Action that runs az ad app credential reset and pushes the secret to ClawCloud nightly; see community PR #5121.
  4. Subscribe to changeNotifications webhooks instead of polling every minute; saves Graph quota.
  5. Open a GitHub discussion if you hit throttling—people share mitigation tricks (back-off headers are quirky).

You now have OpenClaw talking to Outlook mailboxes, creating calendar events, and nudging people in Teams—all with one Graph OAuth token. From here the fun automation ideas stack up fast.