Outlook Calendar (Microsoft Graph, calendar-focused)
Reads and writes the signed-in user's primary calendar through /me/calendarView and
/me/events. This skill uses its own device-code login and token store
(~/.outlook-calendar/). The consent requests exactly three delegated Microsoft
Graph scopes — only what the calendar API needs:
| Scope | Why |
|---|---|
offline_access |
receive a refresh_token so the skill can run headlessly |
https://graph.microsoft.com/User.Read |
display the signed-in account UPN at sign-in and in token status |
https://graph.microsoft.com/Calendars.ReadWrite |
read & write the primary calendar |
Mail, files, notes, directory, and Teams scopes are never requested. The other
Outlook skills (outlook-contacts, outlook-todo) each have their own independent
logins and token stores as well.
⚠️ Safety rules (read these before any write/delete)
- Every write is a two-step confirm.
calendar-write.shdefaults to dry-run: it prints the JSON payload it would send and exits 0. To actually call Graph you must pass--apply, and the script will then ask you to typeYESat the terminal (or pass--yesfor non-interactive use). There is no implicit write and no environment-variable bypass — an agent must pass--yesvisibly. - Delete is extra strict.
deletefirst fetches the event and prints subject / start / end / location / organizer so you can verify, then prompts forYES. The preview uses a$selectprojection — onlyid, subject, start, end, location, organizerare retrieved, so attendee lists and event bodies are never fetched or printed. - No secrets in logs.
access_tokenandrefresh_tokenare never echoed to stdout or stderr. Error responses are sanitized: any field whose name containstoken,refresh,access,secret, orpasswordis replaced with[REDACTED]. - Token & config live under
~/.outlook-calendar/(chmod 700 dir, chmod 600 files). Theclearsubcommand oftoken.shremoves them — it requires--yes-i-really-mean-it.
First-time setup
You need a public client application registration in Entra ID (Azure AD) with "Allow public client flows" = Yes and "Mobile and desktop applications" → device-code flow enabled. The skill uses the device code flow so it works on a headless host.
Azure app registration steps (bring your own — required once):
- Portal → Microsoft Entra ID → App registrations → New registration.
- Supported account types: "Accounts in any organizational directory and personal Microsoft accounts" (covers work/school and Outlook.com).
- No redirect URI needed. In the app → Authentication → set "Allow public client flows" = Yes.
- Copy the Application (client) ID — that is the
--client-idbelow. No client secret is ever required (public client).
The setup script requests exactly these delegated Microsoft Graph scopes:
offline_access, User.Read, Calendars.ReadWrite. No other scopes are requested.
For personal Outlook.com accounts, manually adding Graph API permissions in Azure Portal is usually optional. The setup script requests delegated scopes during login, and Microsoft can grant them via dynamic user consent. For work/school tenants that disable user consent, an administrator may still need to preconfigure permissions and grant admin consent.
cd ~/.openclaw/skills/outlook-calendar
./scripts/setup-device-code.sh --client-id 12345678-1234-1234-1234-123456789012
# follow the printed URL, enter the code, finish the browser sign-in
The script writes:
| File | Mode | Content |
|---|---|---|
~/.outlook-calendar/config.json |
600 | client_id, tenant, authority, scopes, graph_base |
~/.outlook-calendar/tokens.json |
600 | access_token, refresh_token, expires_at, scope |
…and removes the temporary device.json.
Important: never commit
~/.outlook-calendar/to git. Thesetup-device-code.shscript refuses to overwrite existing files unless you pass--force.
Reading events
# Today
./scripts/calendar-read.sh today
# Tomorrow
./scripts/calendar-read.sh tomorrow
# Current calendar week (Mon..Sun in --tz)
./scripts/calendar-read.sh week --tz Asia/Shanghai
# Next 7 days
./scripts/calendar-read.sh next-days 7 --format summary
# Search this week for "standup"
./scripts/calendar-read.sh week --query standup --format ids
# Different start date / timezone
./scripts/calendar-read.sh week --from 2026-06-22 --tz Europe/Berlin
--format options:
json(default) pretty Graph JSONsummarytabular textidsone event id per line (handy for piping intoupdate/delete)rawsingle-line JSON array
Writing events
# 1. Dry-run: see the payload that *would* be sent
./scripts/calendar-write.sh create \
--subject "Lunch with Mei" \
--start "2026-06-18 12:00" \
--end "2026-06-18 13:00" \
--location "Din Tai Fung, Shanghai" \
--body "Discuss Q3 OKRs"
# 2. Real call (with confirmation prompt)
./scripts/calendar-write.sh create \
--subject "Lunch with Mei" \
--start "2026-06-18 12:00" \
--end "2026-06-18 13:00" \
--apply
# ... prints summary, asks: "Type YES within 10s to proceed:" → user types YES
# 3. All-day event
./scripts/calendar-write.sh create \
--subject "Travel to Tokyo" \
--start "2026-06-20" \
--end "2026-06-22" \
--all-day --apply
# 4. Update (only changed fields need to be passed)
./scripts/calendar-write.sh update \
--event-id AAMkAGI2... \
--subject "Lunch (rescheduled)" \
--start "2026-06-18 12:30" \
--end "2026-06-18 13:30" \
--apply
# 5. Delete (always shows the event first)
./scripts/calendar-write.sh delete --event-id AAMkAGI2... --apply
Robust escaping
All user-supplied fields go through jq -n --arg / --argjson (see scripts/_lib.sh
and build_event_body). No string interpolation is used to build the JSON body,
so subjects like She said "hi"\nwith a backslash \ and embedded nulls are safe.
Event IDs get the same treatment: get, update, and delete validate --event-id
(whitespace, control characters, ?, and # are rejected) and percent-encode it
before it is interpolated into any /me/events/{id} URL, so a crafted id cannot
steer the Graph bearer token to another endpoint. Pass the raw id exactly as Graph
returned it (e.g. from calendar-read.sh --format ids) — never pre-encode.
Minimal preview data. The delete preview fetches only id, subject, start, end, location, organizer (via $select) and prints exactly those fields — attendee lists
and body content are never retrieved from Graph or echoed to the terminal.
Token management
# Status (token presence, expiry, /me probe with User.Read only)
./scripts/token.sh status
# Force a refresh (no-op if still valid for >120s)
./scripts/token.sh refresh
# Show scopes the token was actually issued for
./scripts/token.sh scopes
# Nuke local config & tokens (irreversible)
./scripts/token.sh clear --yes-i-really-mean-it
The token.sh script never prints the access_token or refresh_token. The /me probe
uses ?$select=id,userPrincipalName only.
Files in this skill
| Path | Purpose |
|---|---|
SKILL.md |
this file |
scripts/setup-device-code.sh |
one-time device-code flow sign-in |
scripts/token.sh |
inspect / refresh / clear stored tokens |
scripts/calendar-read.sh |
today / week / next-days N over /me/calendarView |
scripts/calendar-write.sh |
create / update / get / delete over /me/events |
scripts/_lib.sh |
shared helpers (sourced; not for direct use) |
references/graph-calendar.md |
Microsoft Graph calendar endpoint reference |
tests/test-local.sh |
offline smoke + JSON escaping tests (no real Microsoft calls) |
Troubleshooting
AADSTS700016: Application with identifier ... was not found— yourclient_idis wrong or your app registration lives in a different tenant than the one you passed via--tenant-id.AADSTS65001: The user or administrator has not consented to ... Calendars.ReadWrite— the tenant likely blocks dynamic user consent. Ask an admin to preconfigure the delegated Graph permissions and grant consent; also verify "Allow public client flows" is enabled.401 Unauthorizedon every call — the access token was likely revoked. Runscripts/token.sh refresh --force; if that fails,scripts/setup-device-code.sh --force.- Empty results in a known-busy week — check
--tz; the default isAsia/Shanghai, and a "week" in your local time may straddle a different set of events than the Graph server's UTC view.
Tests (no real Microsoft credentials required)
cd ~/.openclaw/skills/outlook-calendar
./tests/test-local.sh
Covers: shell syntax (bash -n), jq payload validation for create/update/all-day,
JSON escaping (quotes, backslashes, newlines, control chars, emoji, CJK), dry-run
mode (no network), client-id / tenant-id / scope validation, all-day input
validation, and the clear safety interlock.
