Outlook Contacts (Microsoft Graph, read & write)
A contacts read-and-write Microsoft Graph skill. Reads and writes the signed-in user's personal contacts
through /me/contacts. Does not access mail/files/directory.
This skill has its own device-code login and token store (~/.outlook-contacts/).
The consent requests exactly three delegated Microsoft Graph scopes:
| Scope | Why |
|---|---|
offline_access |
refresh_token for headless operation |
https://graph.microsoft.com/User.Read |
display signed-in account UPN |
https://graph.microsoft.com/Contacts.ReadWrite |
read and write personal contacts |
All write operations (create, update, delete) default to dry-run; you must pass --apply to execute.
⚠️ Safety rules
- Every write defaults to dry-run. Pass
--applyto execute. Create/update print the JSON payload first. - Delete always shows the contact first, then asks for a typed
YESconfirmation (or--yesfor non-interactive use). - No secrets in logs. Tokens are never echoed; error bodies are sanitized.
- Token & config live under
~/.outlook-contacts/(chmod 700 dir, chmod 600 files).
First-time setup
You need a public client application registration in Entra ID (Azure AD) with "Allow public client flows" = Yes and device-code flow enabled. This 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".
- 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 required.
The setup script requests exactly these delegated Microsoft Graph scopes:
offline_access, User.Read, Contacts.ReadWrite.
cd ~/.openclaw/skills/outlook-contacts
./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-contacts/config.json |
600 | client_id, tenant, authority, scopes, graph_base |
~/.outlook-contacts/tokens.json |
600 | access_token, refresh_token, expires_at, scope |
Important: never commit
~/.outlook-contacts/to git. Use--forceto overwrite.
Reading contacts
# List all contacts (paginated, max 200 per page)
./scripts/contacts-read.sh list --format summary
# Search contacts by name, email, or company
./scripts/contacts-read.sh search "jane" --format summary
# Filter by specific field
./scripts/contacts-read.sh list --filter "contains(displayName,'Zhang')" --format json
# Limit results
./scripts/contacts-read.sh list --limit 20 --format summary
# Raw JSON output
./scripts/contacts-read.sh list --format raw
--format options:
summary(default) — compact table: name, email, phone, company, titlejson— pretty-printed full Graph JSONsimple— one line per contact: "name 📞phone"raw— single-line JSON array
Format examples
summary (default):
NAME EMAIL PHONE COMPANY TITLE
----------------------------------------------------------------------------------------------------
Zhang San zhang@example.com 13812345678 ABC Corp Manager
Li Si li@example.com 13987654321 - -
Total: 2 contacts
simple — one line per contact with optional phone icon:
Zhang San <zhang@example.com> 📞13812345678
Li Si <li@example.com>
Search syntax
The search subcommand uses Microsoft Graph's $search query parameter on the
displayName and emailAddresses fields:
./scripts/contacts-read.sh search "Wang" --format simple
./scripts/contacts-read.sh search "gmail.com" --format summary
Filtering
The --filter option passes an OData $filter expression directly:
./scripts/contacts-read.sh list --filter "jobTitle eq 'Professor'" --format summary
./scripts/contacts-read.sh list --filter "startswith(givenName,'X')" --format json
Writing contacts
⚠️ Safety: every write defaults to dry-run. Pass --apply to execute.
# Get a single contact (read-only, no --apply needed)
./scripts/contacts-write.sh get --contact-id "AAMk..."
# Update a contact (dry-run by default; see payload before applying)
./scripts/contacts-write.sh update --contact-id "AAMk..." \
--email "new@example.com" --phone "13812345678"
# Actually update (after confirming dry-run output looks correct)
./scripts/contacts-write.sh update --contact-id "AAMk..." \
--email "new@example.com" --phone "13812345678" --apply
# Create a new contact
./scripts/contacts-write.sh create --display-name "Zhang San" \
--email "zhang@example.com" --phone "13912345678" --apply
# Delete a contact (always shows the contact first, then asks for YES confirmation)
./scripts/contacts-write.sh delete --contact-id "AAMk..." --apply
Update fields available
--display-name— full display name--given-name— first name--surname— last name--email— primary email (replaces existing email array)--phone— mobile phone number--company— company name--job-title— job title--notes— personal notes
Token management
./scripts/token.sh status
./scripts/token.sh refresh
./scripts/token.sh scopes
./scripts/token.sh clear --yes-i-really-mean-it
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/_lib.sh |
shared helpers (sourced; not for direct use) |
scripts/contacts-read.sh |
list / search contacts via /me/contacts |
scripts/contacts-write.sh |
get / update / create / delete contacts |
Troubleshooting
token scope does not include Contacts.ReadWrite— re-run./scripts/setup-device-code.sh --forceto re-authenticate.- Empty results — check that contacts exist in Outlook.com under "People".
401 Unauthorized— runscripts/token.sh refresh --force; if that fails, re-runscripts/setup-device-code.sh --force.
