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
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
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.
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/cal-cli
cd owa-cal
pip install -e '.[test]'
pytest -q
See AGENTS.md for repo layout and ground rules.
Disclaimer
Personal tooling. Stores a delegated refresh token on disk.
If you don't know why that 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.0.tar.gz.
File metadata
- Download URL: owa_cal-0.6.0.tar.gz
- Upload date:
- Size: 28.0 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 |
bd3fe1dbdfebc55472787e9e3fb5ed64ef945c676b4c469ed07c1a2f6c49bd3c
|
|
| MD5 |
4a6fbf8519683a925fba2743403f9fff
|
|
| BLAKE2b-256 |
f38780e749550c6c32dc3473290ceb4cba1ac15aca1f0fac3caf28a8be26f364
|
File details
Details for the file owa_cal-0.6.0-py3-none-any.whl.
File metadata
- Download URL: owa_cal-0.6.0-py3-none-any.whl
- Upload date:
- Size: 23.1 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 |
7248ed12f7d1abc4dc987a5b33cdcc24a754673012f7a5f8b296601cd95fdc22
|
|
| MD5 |
ebbfabe5b60307ff598bfa6791656cd2
|
|
| BLAKE2b-256 |
93fa7ef50c8702b937cfd1a586dc7df8491c5023b195164e010fbe16bcd341ac
|