Pipe-friendly calendar CLI for Outlook / Microsoft 365
Project description
owa-cal
Calendar CLI for Outlook / Microsoft 365. Read, create, update and delete events from the terminal.
Pipe-friendly JSON by default, --pretty for humans.
brew install damsleth/tap/owa-cal
owa-cal events --pretty
Or one-shot, no install, no on-disk state:
OWA_REFRESH_TOKEN=1.AQ... OWA_TENANT_ID=<tenant-id-or-domain> \
uvx owa-cal events --pretty
uvx pulls owa-cal (and owa-piggy as a transitive dep) into a
throwaway venv. The two env vars feed straight through to owa-piggy's
env-only mode - nothing is written to ~/.config/. Useful on a
borrowed laptop, in a CI job, or for a one-off script. See
Single-line uvx for how
to scrape the two values from a browser session.
Happy-path setup (no app registration)
owa-piggy owns the token
lifecycle; owa-cal just shells out to it on every call. The full
first-run flow:
# 1. Install both
brew install damsleth/tap/owa-piggy damsleth/tap/owa-cal
# 2. Seed owa-piggy once from your browser (walks you through it)
owa-piggy setup
# 3. Go
owa-cal events --pretty
owa-piggy and owa-cal version independently. owa-cal expects any owa-piggy >= 0.6.0 and sanity-checks the version on first call.
Multi-account: seed a named owa-piggy profile and pin it in owa-cal's config.
owa-piggy setup --profile work
owa-cal config --profile work
--profile also works as a one-shot override:
owa-cal --profile home events.
Refresh tokens rotate on every call and are persisted by owa-piggy in
its own profile store. owa-cal stores no refresh token on this path;
owa-piggy --reseed --profile <alias> refreshes the token headlessly
when the 24h hard-expiry lapses, and owa-cal picks up the new token
on the next call automatically.
The output contract
JSON on stdout, logs on stderr. Every read command emits parseable
JSON by default; --pretty is a human override that goes to stdout
too. That means the entire CLI composes with jq:
owa-cal events
[
{
"id": "AAMkAGI1...redacted",
"subject": "Standup",
"start": "2026-04-20T09:00:00",
"end": "2026-04-20T09:30:00",
"categories": ["ProjectX"],
"location": "Teams",
"showAs": "Busy",
"isAllDay": false
},
{
"id": "AAMkAGI2...redacted",
"subject": "Lunsj",
"start": "2026-04-20T11:00:00",
"end": "2026-04-20T11:30:00",
"categories": ["CC LUNCH"],
"location": "",
"showAs": "Busy",
"isAllDay": false
}
]
Timestamps are normalized to your local timezone. Field names in the output are stable lowercase; the backend is Outlook REST v2 (PascalCase upstream) but owa-cal hides that detail.
owa-cal events | jq '.[].subject'
owa-cal events --date tomorrow | jq '[.[] | select(.showAs == "Busy")] | length'
owa-cal events --week 16 | jq 'group_by(.start | .[0:10]) | map({day: .[0].start[0:10], count: length})'
Same shape on create / update (returns the single normalized
event), and on categories (returns [{"name": ..., "color": ...}]).
Commands
owa-cal events --pretty # today
owa-cal events --week 16 --pretty # ISO week
owa-cal events --from 2026-04-14 --to 2026-04-18 --pretty
owa-cal events --search "standup" --pretty
owa-cal create --subject "lunsj" --start 11:00 --end 11:30 --category "CC LUNCH"
owa-cal update --id <event-id> --category "ProjectX"
owa-cal delete --id <event-id>
owa-cal categories # JSON
owa-cal categories --pretty # aligned table
Auth
Two paths:
- owa-piggy bridge (default) - owa-cal shells out to
owa-piggy, which piggybacks on OWA's public SPA client. No app registration needed; owa-cal stores no refresh token. Optionalowa_piggy_profilepins a named owa-piggy profile. - With an app registration - set
OUTLOOK_APP_CLIENT_ID,OUTLOOK_REFRESH_TOKEN, andOUTLOOK_TENANT_IDin the config file and owa-cal talks to the AAD token endpoint directly.
Config lives at ~/.config/owa-cal/config:
# Default (owa-piggy) path - optional, pins a profile alias
owa_piggy_profile="work"
# App-registration path (optional, mutually exclusive)
OUTLOOK_APP_CLIENT_ID=""
OUTLOOK_REFRESH_TOKEN=""
OUTLOOK_TENANT_ID=""
OUTLOOK_APP_CLIENT_ID can be overridden via the environment. The
refresh token / tenant id on the app-registration path live
exclusively in the config file.
Single-line uvx (no install, no disk state)
uvx owa-cal pulls both packages into an ephemeral venv and never
writes to ~/.config/. Pair it with owa-piggy's env-only mode and
you have a one-shot, fully portable invocation:
OWA_REFRESH_TOKEN=1.AQ... \
OWA_TENANT_ID=<tenant-id-or-domain> \
uvx owa-cal events --pretty
Variables go to owa-piggy via subprocess env inheritance; owa-cal
itself never sees the token. OWA_PROFILE is honored if you also
have profiles on disk, but is unnecessary in env-only mode.
To scrape the two values out of a browser session (Edge -> outlook.cloud.microsoft, F12 -> Console):
const find = s => Object.keys(localStorage).find(k => k.includes(s))
const parse = s => JSON.parse(localStorage[find(s)])
const rt = parse('|refreshtoken|'), it = parse('|idtoken|')
console.log(`OWA_REFRESH_TOKEN=${rt.secret || rt.data}
OWA_TENANT_ID=${it.realm || find('|idtoken|').split('|')[5]}`)
Caveats:
- Plain Chromium browsers (vanilla Chrome/Brave) store a session-bound token AAD won't accept. Use Microsoft Edge.
- The refresh token AAD returns rotates on every exchange. In env-only
mode owa-piggy prints a
NOTE:to stderr noting the new token; copy it back into your env if you plan another call. Persistent use belongs inowa-piggy setup, not env vars. - Tokens on a command line (e.g.
OWA_REFRESH_TOKEN=... uvx ...) end up in shell history andps aux. Source them from a file (set -a; . secrets.env; set +a; uvx owa-cal events) or your password manager's CLI.
For agents
The same invocation is the cleanest way for an LLM agent or automation to read/write a calendar without persistent setup:
OWA_REFRESH_TOKEN=$RT OWA_TENANT_ID=$TID \
uvx --quiet owa-cal events --from 2026-04-26 --to 2026-05-03
Useful contract for agent code:
- stdout is JSON (omit
--pretty); stderr is logs. - exit
0success,1any failure (auth, network, validation). --quietonuvxsuppresses theInstalled N packagesline so stdout stays clean forjq/json.loads.- pin a version for reproducibility:
uvx --from 'owa-cal==0.6.1' owa-cal events. - short-lived only. Refresh tokens rotate on every exchange and have
nowhere to go in env-only mode; for an agent that calls more than
once across the 24h sliding window, run
owa-piggy setup --profile agentonce on the host and useOWA_PROFILE=agent uvx owa-cal ...instead - owa-piggy then handles rotation and caching.
Dependencies
- Python 3.8+ (stdlib only - no
pip installrequired at runtime) owa-piggyunless you bring your own app registration
Development
git clone https://github.com/damsleth/owa-cal
cd owa-cal
pip install -e '.[test]'
pytest -q
See AGENTS.md for repo layout and ground rules.
Disclaimer
Personal tooling. The default (owa-piggy bridge) path holds no
refresh token of its own - tokens are owa-piggy's responsibility,
scoped to its profile store. The optional app-registration path
does persist a delegated refresh token in owa-cal's config file.
If you don't know why either of those might be a bad idea, don't
use it.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file owa_cal-0.6.2.tar.gz.
File metadata
- Download URL: owa_cal-0.6.2.tar.gz
- Upload date:
- Size: 32.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5343d45abb5c88d35735f9c772b52f24ca5c0cfb979cbdfee263708cbee5a961
|
|
| MD5 |
e16dcb4941a05ac9fa57305ceba13f5f
|
|
| BLAKE2b-256 |
ba0e8e0ef7c4795bbbb772af7740ac56c431c670cb9be296be472b28a2e9a4b6
|
File details
Details for the file owa_cal-0.6.2-py3-none-any.whl.
File metadata
- Download URL: owa_cal-0.6.2-py3-none-any.whl
- Upload date:
- Size: 25.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf2293032125dcaf48fdf12b54a78c8452076a0ace2c3447ed38e7113ee0d935
|
|
| MD5 |
e571ae28bce0735f030c2532ea440239
|
|
| BLAKE2b-256 |
e9bc78545fb680544ab1c172b1f9ecca7f42884c41225b924c47a847b31bcae2
|