Microsoft 365 Graph for OpenClaw Skill
1. Quick prerequisites
- Python 3 with
requestsinstalled. - 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.
- Client ID (personal-account default):
- Tokens are stored in
state/graph_auth.json(ignored by git). - Required runtime envs for push mode:
OPENCLAW_HOOK_URLOPENCLAW_HOOK_TOKENGRAPH_WEBHOOK_CLIENT_STATEOPENCLAW_SESSION_KEY
Permission profiles (least privilege by use case) are documented in docs/permission-profiles.md.
2. Assisted OAuth flow (Device Code)
- Run:
python scripts/graph_auth.py device-login \ --client-id 952d1b34-682e-48ce-9c54-bac5a96cbd42 \ --tenant-id consumers - The script prints a URL and device code.
- Open
https://microsoft.com/devicelogin, enter the code, and approve with the target account. - Check and manage auth state:
python scripts/graph_auth.py statuspython scripts/graph_auth.py refreshpython scripts/graph_auth.py clear
- Other scripts call
utils.get_access_token(), which refreshes tokens automatically when needed. - Scope override is disabled in
graph_auth.py; the skill always usesDEFAULT_SCOPES.
Detailed reference: 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 (
saveToSentItemsenabled by default):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-copyonly when you intentionally do not want Sent Items storage.
More examples and filters: references/mail.md.
4. Calendar operations
- List custom date window:
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--onlinefor 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_idreturned in JSON output.
Full examples: 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
moveandsharesubcommands. - The script resolves localized/special-folder aliases (for example
DocumentsandDocumentos).
More details: 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.
7. Mail push mode (Webhook Adapter)
- Adapter server (Graph handshake +
clientStatevalidation + enqueue):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):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--resourceonly for advanced/scoped scenarios.
- Default resource is
- Async worker (dedupe + default wake signal to OpenClaw
/hooks/wake):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 agentonly when you explicitly need per-message rich payload delivery.
- Default mode is
- Worker queue files:
state/mail_webhook_queue.jsonlstate/mail_webhook_dedupe.json
- Automated EC2 bootstrap (Caddy + systemd + renew timer):
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-runto preview all privileged writes and service actions before applying changes.
- Use
- One-command setup (steps 2..6):
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-runfor a no-mutation execution plan (no/etcwrites, nosystemctl, no subscription create, no email send). - Output ends with
READY_FOR_PUSH: YESwhen setup is fully validated.
- Use
- Include OpenClaw hook config in automation:
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:
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_PUSHonly after all critical checks pass.
- Output ends with
- Setup and runbook:
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.shscripts/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/*.serviceand*.timer - Enable/restart services via
systemctl - Optionally patch OpenClaw config and restart OpenClaw services
Recommended safety sequence:
- Run with
--dry-run - Review emitted actions and target files
- Run on a non-production host first
- Apply to production only after approval
9. Logging and conventions
- Each script appends one JSON line to
state/graph_ops.logwith 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.