Microsoft 365 Graph for OpenClaw with webhook-based wake signals. Reduce recurring LLM cost from inbox polling while managing Outlook mail, calendar, OneDriv...
---
name: microsoft-365-graph-openclaw
description: Microsoft 365 Graph for OpenClaw with webhook-based wake signals. Reduce recurring LLM cost from inbox polling while managing Outlook mail, calendar, OneDrive, and contacts via Microsoft Graph.
version: 0.2.2
license: MIT
homepage: https://github.com/draeden79/microsoft-365-graph-openclaw
repository: https://github.com/draeden79/microsoft-365-graph-openclaw
metadata: {"openclaw":{"homepage":"https://github.com/draeden79/microsoft-365-graph-openclaw","os":["linux","darwin","win32"],"requires":{"bins":["python3","bash","curl"]}}}
security:
summary: Push-first Graph integration with explicit hook token auth and clientState validation.
notes:
- Do not commit state/graph_auth.json or token-bearing logs.
- Keep hooks token and Graph clientState in protected env storage.
- Prefer /hooks/wake default to avoid unnecessary isolated agent runs.
---
# Microsoft 365 Graph for OpenClaw Skill
## 1. Quick prerequisites
1. Python 3 with `requests` installed.
2. Default auth values:
- Client ID (personal-account default): `952d1b34-682e-48ce-9c54-bac5a96cbd42`
- Tenant (personal-account default): `consumers`
- Default scopes: `Mail.ReadWrite Mail.Send Calendars.ReadWrite Files.ReadWrite.All Contacts.ReadWrite offline_access`
- For work/school accounts, use `--tenant-id organizations` (or tenant GUID) and a tenant-approved `--client-id`.
- The public default client ID is for quick testing. For production, prefer your own App Registration.
3. Tokens are stored in `state/graph_auth.json` (ignored by git).
4. Push-mode runtime values (service-level):
- These values are loaded by systemd services from `/etc/default/graph-mail-webhook` (usually written by setup scripts).
- `OPENCLAW_HOOK_URL` (required)
- `OPENCLAW_HOOK_TOKEN` (required)
- `GRAPH_WEBHOOK_CLIENT_STATE` (required; auto-generated in minimal e2e setup when omitted)
- `OPENCLAW_SESSION_KEY` (optional; default `hook:graph-mail`)
Permission profiles (least privilege by use case) are documented in `docs/permission-profiles.md`.
## 2. Assisted OAuth flow (Device Code)
1. Run:
```bash
python scripts/graph_auth.py device-login \
--client-id 952d1b34-682e-48ce-9c54-bac5a96cbd42 \
--tenant-id consumers
```
2. The script prints a **URL** and **device code**.
3. Open `https://microsoft.com/devicelogin`, enter the code, and approve with the target account.
4. Check and manage auth state:
- `python scripts/graph_auth.py status`
- `python scripts/graph_auth.py refresh`
- `python scripts/graph_auth.py clear`
5. Other scripts call `utils.get_access_token()`, which refreshes tokens automatically when needed.
6. Scope override is disabled in `graph_auth.py`; the skill always uses `DEFAULT_SCOPES`.
Detailed reference: [`references/auth.md`](references/auth.md).
## 3. Email operations
- **List/filter**: `python scripts/mail_fetch.py --folder Inbox --top 20 --unread`
- **Fetch specific message**: `... --id <messageId> --include-body --mark-read`
- **Move message**: add `--move-to <folderId>` to the command above.
- **Send email** (`saveToSentItems` enabled by default):
```bash
python scripts/mail_send.py \
--to user@example.com \
--subject "Update" \
--body-file replies/thais.html --html \
--cc teammate@example.com \
--attachment docs/proposal.pdf
```
- Use `--no-save-copy` only when you intentionally do not want Sent Items storage.
More examples and filters: [`references/mail.md`](references/mail.md).
## 4. Calendar operations
- **List custom date window**:
```bash
python scripts/calendar_sync.py list \
--start 2026-03-03T00:00Z --end 2026-03-05T23:59Z --top 50
```
- **Create Teams or in-person event**: use `create`; add `--online` for Teams link.
- For personal Microsoft accounts (`tenant=consumers`), Teams meeting provisioning via Graph might not return a join URL; create the Teams meeting in Outlook/Teams first and add the resulting link to the event body when needed.
- **Update/cancel** events by `event_id` returned in JSON output.
Full examples: [`references/calendar.md`](references/calendar.md).
## 5. OneDrive / Files
- **List folders/files**: `python scripts/drive_ops.py list --path /`
- **Upload**: `... upload --local notes/briefing.docx --remote /Clients/briefing.docx`
- **Download**: `... download --remote /Clients/briefing.docx --local /tmp/briefing.docx`
- **Move / share links**: use `move` and `share` subcommands.
- The script resolves localized/special-folder aliases (for example `Documents` and `Documentos`).
More details: [`references/drive.md`](references/drive.md).
## 6. Contacts
- **List/search**: `python scripts/contacts_ops.py list --top 20`
- **Create**: `... create --given-name Jane --surname Doe --email jane.doe@example.com`
- **Update/Delete**: `... update <contactId> ...` / `... delete <contactId>`
- Contacts are part of the default scope set and supported as a first-class workflow.
More details: [`references/contacts.md`](references/contacts.md).
## 7. Mail push mode (Webhook Adapter)
- **Adapter server** (Graph handshake + `clientState` validation + enqueue):
```bash
python scripts/mail_webhook_adapter.py serve \
--host 0.0.0.0 --port 8789 --path /graph/mail \
--client-state "$GRAPH_WEBHOOK_CLIENT_STATE"
```
- **Subscription lifecycle** (`create/status/renew/delete/list`):
```bash
python scripts/mail_subscriptions.py create \
--notification-url "https://graph-hook.example.com/graph/mail" \
--client-state "$GRAPH_WEBHOOK_CLIENT_STATE" \
--minutes 4200
```
- Default resource is `me/messages` (recommended for better delivery coverage). Override with `--resource` only for advanced/scoped scenarios.
- **Async worker** (dedupe + default wake signal to OpenClaw `/hooks/wake`):
```bash
python scripts/mail_webhook_worker.py loop \
--session-key "$OPENCLAW_SESSION_KEY" \
--hook-url "$OPENCLAW_HOOK_URL" \
--hook-token "$OPENCLAW_HOOK_TOKEN"
```
- Default mode is `wake` (`/hooks/wake`, `mode=now`). Use `--hook-action agent` only when you explicitly need per-message rich payload delivery.
- Worker queue files:
- `state/mail_webhook_queue.jsonl`
- `state/mail_webhook_dedupe.json`
- **Automated EC2 bootstrap** (Caddy + systemd + renew timer):
```bash
sudo bash scripts/setup_mail_webhook_ec2.sh \
--domain graphhook.example.com \
--hook-url http://127.0.0.1:18789/hooks/wake \
--hook-token "<OPENCLAW_HOOK_TOKEN>" \
--session-key "hook:graph-mail" \
--client-state "<GRAPH_WEBHOOK_CLIENT_STATE>" \
--repo-root "$(pwd)"
```
- Use `--dry-run` to preview all privileged writes and service actions before applying changes.
- **One-command setup (steps 2..6)**:
```bash
sudo bash scripts/run_mail_webhook_e2e_setup.sh \
--domain graphhook.example.com \
--hook-token "<OPENCLAW_HOOK_TOKEN>" \
--hook-url "http://127.0.0.1:18789/hooks/wake" \
--session-key "hook:graph-mail" \
--test-email "tar.alitar@outlook.com"
```
- Use `--dry-run` for a no-mutation execution plan (no `/etc` writes, no `systemctl`, no subscription create, no email send).
- Output ends with `READY_FOR_PUSH: YES` when setup is fully validated.
- **Include OpenClaw hook config in automation**:
```bash
sudo bash scripts/run_mail_webhook_e2e_setup.sh \
--domain graphhook.example.com \
--hook-token "<OPENCLAW_HOOK_TOKEN>" \
--configure-openclaw-hooks \
--openclaw-config "/home/ubuntu/.openclaw/openclaw.json" \
--openclaw-service-name "auto" \
--openclaw-hooks-path "/hooks" \
--openclaw-allow-request-session-key true \
--test-email "tar.alitar@outlook.com"
```
- **Minimal-input smoke tests**:
```bash
sudo bash scripts/run_mail_webhook_smoke_tests.sh \
--domain graphhook.example.com \
--create-subscription \
--test-email tar.alitar@outlook.com
```
- Output ends with `READINESS VERDICT: READY_FOR_PUSH` only after all critical checks pass.
- Setup and runbook: [`references/mail_webhook_adapter.md`](references/mail_webhook_adapter.md).
## 8. Privileged operations boundary
The core Graph scripts are unprivileged (`graph_auth.py`, `mail_fetch.py`, `mail_send.py`, `calendar_sync.py`, `drive_ops.py`, `contacts_ops.py`).
The setup scripts below are privileged and should be manually reviewed before execution:
- `scripts/setup_mail_webhook_ec2.sh`
- `scripts/run_mail_webhook_e2e_setup.sh`
When run without `--dry-run`, they can:
- Write `/etc/default/graph-mail-webhook`
- Write `/etc/caddy/Caddyfile`
- Write `/etc/systemd/system/*.service` and `*.timer`
- Enable/restart services via `systemctl`
- Optionally patch OpenClaw config and restart OpenClaw services
Recommended safety sequence:
1. Run with `--dry-run`
2. Review emitted actions and target files
3. Run on a non-production host first
4. Apply to production only after approval
## 9. Logging and conventions
- Each script appends one JSON line to `state/graph_ops.log` with timestamp, action, and key parameters.
- Tokens and logs must never be committed.
- Commands assume execution from the repository root. Adjust paths if running elsewhere.
## 10. Troubleshooting
| Symptom | Action |
| --- | --- |
| 401/invalid_grant | Run `graph_auth.py refresh`; if it fails, run `clear` and repeat device login. |
| 403/AccessDenied | Missing scope or licensing/policy issue. Re-run device login with required scope(s). |
| 429/Throttled | Scripts do basic retry; wait a few seconds and retry. |
| `requests.exceptions.SSLError` | Verify local system date/time and TLS trust chain. |
This skill provides OAuth-driven workflows for email, calendar, files, contacts, and push-based mail automation via Microsoft Graph.
don't have the plugin yet? install it then click "run inline in claude" again.
reorganized original loose reference docs into explicit six-component structure with detailed decision points for auth failures, rate limits, webhook delivery debugging, and permission scenarios; clarified all inputs including env vars and external connections; added edge case handling and safety guidance for privileged scripts; preserved all original procedures and scripts faithfully.
this skill surfaces Microsoft 365 resources (mail, calendar, files, contacts) via the Microsoft Graph API, with optional webhook-based push notifications to openclaw that trigger on incoming mail instead of polling. use this when you need to read/write outlook data, sync calendars, manage onedrive files, or minimize per-message LLM inference cost by reacting to real-time mail events.
runtime requirements:
requests library installedexternal connections: microsoft 365 / microsoft graph
oauth via device code flow
952d1b34-682e-48ce-9c54-bac5a96cbd42consumers--tenant-id organizations or your tenant GUID with an organization-approved client idMail.ReadWrite Mail.Send Calendars.ReadWrite Files.ReadWrite.All Contacts.ReadWrite offline_accessstate/graph_auth.json (git-ignored)webhook mode (optional, push-based)
/etc/default/graph-mail-webhook):OPENCLAW_HOOK_URL (required): base URL of openclaw instanceOPENCLAW_HOOK_TOKEN (required): bearer token for /hooks/wake endpointGRAPH_WEBHOOK_CLIENT_STATE (required): opaque validation token for graph webhook payloadsOPENCLAW_SESSION_KEY (optional, default hook:graph-mail): session key for dedupe and routingstate/mail_webhook_queue.jsonl and state/mail_webhook_dedupe.jsonsetup scripts (privileged, ec2/linux only)
/etc/default/graph-mail-webhook, /etc/caddy/Caddyfile, /etc/systemd/system/permission profiles
docs/permission-profiles.mdinputs: client id, tenant id (optional)
python scripts/graph_auth.py device-login \
--client-id 952d1b34-682e-48ce-9c54-bac5a96cbd42 \
--tenant-id consumers
action: prints a device code and url to https://microsoft.com/devicelogin. open url in browser, enter code, approve scope request with target account.
outputs: access token + refresh token written to state/graph_auth.json. token refresh happens automatically on subsequent calls.
manage auth state:
python scripts/graph_auth.py status: print current token expiry and refresh statuspython scripts/graph_auth.py refresh: force token refreshpython scripts/graph_auth.py clear: wipe auth state (requires re-login next time)note: scope override is disabled; skill always uses DEFAULT_SCOPES. to use different scopes, edit DEFAULT_SCOPES in graph_auth.py before device login. full auth reference: references/auth.md.
inputs: folder name, filter options (unread, top N, include body), message id (optional)
list/filter messages:
python scripts/mail_fetch.py --folder Inbox --top 20 --unread
fetch specific message with body:
python scripts/mail_fetch.py --id <messageId> --include-body --mark-read
move message to folder:
python scripts/mail_fetch.py --id <messageId> --move-to <folderId>
outputs: json array of message objects (id, subject, from, received datetime, unread status, body if requested). folder operations return success/error status. mark-read and move operations return updated message state. full mail reference: references/mail.md.
inputs: recipient email, subject, body (plain text or html file), cc/bcc (optional), attachments (optional), save-to-sent flag (default true)
python scripts/mail_send.py \
--to user@example.com \
--subject "Update" \
--body-file replies/thais.html --html \
--cc teammate@example.com \
--attachment docs/proposal.pdf
outputs: message id and sent timestamp. if --no-save-copy is used, message is not stored in Sent Items. default behavior saves copy to Sent Items.
inputs: start datetime (iso8601 utc), end datetime (iso8601 utc), top N (optional)
python scripts/calendar_sync.py list \
--start 2026-03-03T00:00Z --end 2026-03-05T23:59Z --top 50
outputs: json array of event objects (id, title, start, end, attendees, online meeting url if present, organizer).
inputs: title, start datetime, end datetime, description (optional), online flag (optional, creates teams meeting), location (optional)
python scripts/calendar_sync.py create \
--title "Team Sync" \
--start 2026-03-04T14:00Z --end 2026-03-04T15:00Z \
--description "Weekly standup" \
--online
outputs: event id, created event object with teams meeting url (if --online used and tenant supports it). note: personal accounts (consumers) may not return join url from graph; create in outlook/teams first and add url to body when needed.
update/cancel event:
python scripts/calendar_sync.py update <event_id> --title "New Title"
python scripts/calendar_sync.py cancel <event_id>
full calendar reference: references/calendar.md.
inputs: local path, remote path, operation (list/upload/download/move/share)
list folders and files:
python scripts/drive_ops.py list --path /
upload file:
python scripts/drive_ops.py upload --local notes/briefing.docx --remote /Clients/briefing.docx
download file:
python scripts/drive_ops.py download --remote /Clients/briefing.docx --local /tmp/briefing.docx
move/rename:
python scripts/drive_ops.py move --remote /old-name.docx --new-path /new-name.docx
share link:
python scripts/drive_ops.py share --remote /file.docx
outputs: file metadata (id, name, size, modified date), sharing link. script resolves localized folder aliases (e.g., Documents and Documentos). full drive reference: references/drive.md.
inputs: given name, surname, email, phone (optional)
list contacts:
python scripts/contacts_ops.py list --top 20
search contacts:
python scripts/contacts_ops.py list --search "john"
create contact:
python scripts/contacts_ops.py create --given-name Jane --surname Doe --email jane.doe@example.com
update contact:
python scripts/contacts_ops.py update <contactId> --email newemail@example.com
delete contact:
python scripts/contacts_ops.py delete <contactId>
outputs: contact id, full contact object with all fields. full contacts reference: references/contacts.md.
this step is optional. use only if you want to reduce polling and react to incoming mail in real-time.
step 8a: start webhook listener (no privilege required)
inputs: host, port, path, client-state token
python scripts/mail_webhook_adapter.py serve \
--host 0.0.0.0 --port 8789 --path /graph/mail \
--client-state "$GRAPH_WEBHOOK_CLIENT_STATE"
action: binds to host:port, listens for graph webhook payloads at /path, validates clientState, enqueues messages to state/mail_webhook_queue.jsonl.
outputs: running http server. logs all inbound notifications and validation results.
step 8b: create graph subscription
inputs: notification url (https), client-state token, subscription lifetime in minutes
python scripts/mail_subscriptions.py create \
--notification-url "https://graph-hook.example.com/graph/mail" \
--client-state "$GRAPH_WEBHOOK_CLIENT_STATE" \
--minutes 4200
action: registers subscription with graph api for resource me/messages (default; covers all mail folders). graph will deliver notifications to the url.
outputs: subscription id, expiry time, resource being monitored.
manage subscriptions:
python scripts/mail_subscriptions.py status --subscription-id <id>
python scripts/mail_subscriptions.py renew --subscription-id <id> --minutes 4200
python scripts/mail_subscriptions.py delete --subscription-id <id>
python scripts/mail_subscriptions.py list
step 8c: start async worker (polls queue, sends wake signals)
inputs: session key, openclaw hook url, openclaw hook token
python scripts/mail_webhook_worker.py loop \
--session-key "$OPENCLAW_SESSION_KEY" \
--hook-url "$OPENCLAW_HOOK_URL" \
--hook-token "$OPENCLAW_HOOK_TOKEN"
action: polls state/mail_webhook_queue.jsonl for new entries. deduplicates via state/mail_webhook_dedupe.json. sends wake signal to $HOOK_URL/hooks/wake (mode=now) for each unique message. worker runs continuously.
outputs: wake signals posted to openclaw. dedupe state maintains message ids to prevent duplicate signals. logs all sends.
step 8d: automated setup (ec2 + systemd + caddy)
privileged operation. review carefully before running.
inputs: domain, hook url, hook token, session key, client-state, repo root
sudo bash scripts/setup_mail_webhook_ec2.sh \
--domain graphhook.example.com \
--hook-url http://127.0.0.1:18789/hooks/wake \
--hook-token "<OPENCLAW_HOOK_TOKEN>" \
--session-key "hook:graph-mail" \
--client-state "<GRAPH_WEBHOOK_CLIENT_STATE>" \
--repo-root "$(pwd)"
action (with --dry-run first): writes /etc/default/graph-mail-webhook, sets up caddy reverse proxy with tls, creates systemd services for adapter and worker, enables auto-renewal of graph subscription via systemd timer.
outputs: systemd services running. tls certificate provisioned. environment config in place. full webhook setup reference: references/mail_webhook_adapter.md.
step 8e: one-command e2e setup
privileged operation. use --dry-run first.
sudo bash scripts/run_mail_webhook_e2e_setup.sh \
--domain graphhook.example.com \
--hook-token "<OPENCLAW_HOOK_TOKEN>" \
--hook-url "http://127.0.0.1:18789/hooks/wake" \
--session-key "hook:graph-mail" \
--test-email "tar.alitar@outlook.com"
action: combines all setup steps (oauth, adapter, subscription, worker, systemd, caddy). sends test email to verify end-to-end delivery.
outputs: printed config summary, READY_FOR_PUSH: YES on success, READY_FOR_PUSH: NO with error details on failure.
optional: integrate with openclaw config:
sudo bash scripts/run_mail_webhook_e2e_setup.sh \
--domain graphhook.example.com \
--hook-token "<OPENCLAW_HOOK_TOKEN>" \
--configure-openclaw-hooks \
--openclaw-config "/home/ubuntu/.openclaw/openclaw.json" \
--openclaw-service-name "auto" \
--openclaw-hooks-path "/hooks" \
--openclaw-allow-request-session-key true \
--test-email "tar.alitar@outlook.com"
action: additionally patches openclaw.json to register the /hooks/wake endpoint and restarts openclaw service.
step 8f: smoke tests
privileged operation.
sudo bash scripts/run_mail_webhook_smoke_tests.sh \
--domain graphhook.example.com \
--create-subscription \
--test-email tar.alitar@outlook.com
action: validates adapter is listening, graph can reach callback url, subscription was created, test email can be sent and received by adapter, dedupe and queue logic work.
outputs: READINESS VERDICT: READY_FOR_PUSH if all checks pass, READINESS VERDICT: NOT_READY with failure reasons otherwise.
each core script appends one json line to state/graph_ops.log with timestamp, action, key parameters, and result status. never commit tokens or logs. run all commands from repo root; adjust paths if running elsewhere.
if using personal account (microsoft.com):
consumers and public client idif using work/school account:
--tenant-id organizations or explicit tenant guidif auth token is invalid (401/invalid_grant):
python scripts/graph_auth.py refreshpython scripts/graph_auth.py clear and repeat device login from step 1if permission denied (403/AccessDenied):
if rate limited (429/Throttled):
if ssl/tls error (requests.exceptions.SSLError):
if webhook adapter is running but openclaw not receiving signals:
$OPENCLAW_HOOK_TOKEN matches openclaw bearer token$OPENCLAW_HOOK_URL is reachable from adapter hostGRAPH_WEBHOOK_CLIENT_STATE matches graph subscription payloadif graph subscription is not delivering notifications:
mail_subscriptions.py listmail_subscriptions.py renew --subscription-id <id>if duplicate wake signals are sent to openclaw:
state/mail_webhook_dedupe.json) is corrupted or not persistingbefore running privileged setup scripts:
--dry-run first to review all actions/etc/default/, /etc/systemd/, /etc/caddy/)mail fetch outputs: json array of message objects with keys: id, subject, from, receivedDateTime, isRead, bodyPreview, body (if --include-body).
mail send outputs: json object with keys: id, sentDateTime.
calendar list outputs: json array of event objects with keys: id, subject, start, end, attendees, isOnlineMeeting, onlineMeetingUrl (if online), organizer.
calendar create outputs: json object with full event details including id, onlineMeetingUrl (if applicable).
drive list outputs: json array of file/folder objects with keys: id, name, size, lastModifiedDateTime, folder (boolean), webUrl.
drive upload/download outputs: json object with file metadata and operation status.
contacts list/create/update outputs: json object or array with keys: id, displayName, givenName, surname, emailAddresses, mobilePhone.
webhook adapter: http server listening on specified host:port. logs json lines to stdout with request received, validation status, queue written status.
webhook worker: continuously running, appends dedupe state to json file, posts http post requests to openclaw hook url.
auth state: state/graph_auth.json contains access_token, refresh_token, expires_at. managed by utils.get_access_token().
operation log: state/graph_ops.log contains one json line per operation (mail, calendar, drive, contacts, auth, subscription, adapter, worker).
webhook queue: state/mail_webhook_queue.jsonl contains one json line per incoming notification (message id, timestamp, graph payload).
webhook dedupe: state/mail_webhook_dedupe.json contains set of processed message ids to prevent duplicate signals.
privileged setup outputs: /etc/default/graph-mail-webhook (env vars), /etc/systemd/system/graph-mail-webhook-adapter.service, /etc/systemd/system/graph-mail-webhook-worker.service, /etc/systemd/system/graph-mail-subscriptions.timer, /etc/caddy/Caddyfile (reverse proxy config). services are enabled and running.
auth success: graph_auth.py status prints "Valid" and expiry time in the future. subsequent scripts run without 401 errors.
mail operations success: fetch returns message list; send returns sent message id; move returns success status.
calendar operations success: list returns events in date range; create returns event id with meeting url (if online); update/cancel return updated event state.
drive operations success: list returns folders/files; upload/download complete without io errors; share returns public link.
contacts operations success: list returns contact array; create/update return contact id; delete returns no error.
webhook adapter success: server binds to host:port, logs "listening on port X", receives graph payloads, validates clientState, writes to queue.jsonl.
webhook worker success: worker loops continuously, deduplicates entries, posts wake signals to openclaw hook url, logs "signal sent" for each unique message.
smoke tests success: test prints READINESS VERDICT: READY_FOR_PUSH and all sub-checks pass (listener ok, connectivity ok, subscription created, test email delivered, queue populated).
e2e setup success: setup script prints READY_FOR_PUSH: YES, all systemd services are active/running, tls certificate is valid, graph subscription is registered.
credits: original skill authored and maintained at https://github.com/draeden79/microsoft-365-graph-openclaw. enriched for implexa standards to clarify inputs, decision points, and output contracts.