Skip to the content.

Every subcommand of case-calendar. All commands accept -c / --config to point at a non-default config file:

uv run case-calendar -c production.yaml sync

← Back to docs

sync — pull updates from CourtListener

Backfills new docket entries for every case in config.yaml, extracts hearings and deadlines via the LLM, and re-emits affected calendars.

uv run case-calendar sync
Flag Purpose
--case <case_id> Sync only this one case.
--only-new Sync only cases whose dockets aren’t yet in the store — useful after adding new cases to config.yaml without remembering their ids.
--no-emit Skip the auto-emit at the end of the sync. Rare; mostly for tests.
--force-summaries Regenerate every AI case summary in the same sync. Use after a model or prompt change so the CourtListener session is reused instead of running summarize --force separately.
--reverify Force the per-row verify and dedupe sweeps to run even when no docket changed. By default a sync skips them for any case whose dockets all short-circuit (their verdicts can’t change without new entries). Use after a verify-prompt or model change, or after an out-of-band store edit (reprocess_entries.py / classify_significance.py) that altered rows without advancing any docket.

A three-tier short-circuit keeps quiet days cheap: the docket-level date_modified cutoff, the per-entry modified_after filter, and the content fingerprint dedup mean an unchanged docket costs roughly one cheap CourtListener request and zero LLM calls. The end-of-sync verify and dedupe sweeps are gated on the same signal — a case whose dockets all short-circuit skips them entirely, because their inputs come from the local store and can’t have changed without new entries — so a fully-quiet sync makes no LLM calls at all. (Webhook deliveries don’t advance the stored cutoff, so a docket touched by serve always re-runs its sweeps on the next poll.) Pass --reverify to force the sweeps regardless.

Run on a cron once you’re past the initial backfill — every five minutes is fine; once an hour is plenty for most cases. Or skip cron entirely and use real-time webhooks.

reconcile — catch enriched placeholder entries cheaply

A docket-alert webhook delivers a new entry once, the moment it’s docketed — often as a stub whose document text isn’t available yet (an empty description plus a document that isn’t on RECAP). CourtListener fills the text in and makes the document available shortly after, but it fires only an updated email alert for that change, not a second webhook (CourtListener issue #7423). So serve never sees the enriched version, and a hearing or deadline whose date lives only in the filled-in text can be missed until a poll re-reads it. reconcile closes that gap without the per-docket cost of a full sync: it re-checks only the entries that arrived as placeholders, one CourtListener request each, so its cost scales with recent filing activity rather than with the size of your caseload.

uv run case-calendar reconcile
Flag Purpose
--case <case_id> Reconcile only this one case.
--days <n> Only re-check placeholder entries filed within this many days (default 7). Bounds retries on stubs that never enrich — a placeholder older than the window drops out of scope.
--no-emit Skip the auto-emit at the end of the reconcile.

When a placeholder has enriched, the re-fetch re-runs the normal pipeline: the entry’s fingerprint flips, any new hearing or deadline is extracted, and the case summary is regenerated if its posture changed — exactly as a sync would, but touching only the handful of pending entries. An unchanged placeholder is a no-op (the fingerprint matches, so no LLM call is made).

Intended to run on a frequent cheap cron alongside serve, with a full sync kept as an infrequent catch-all. See real-time webhooks for how the three fit together.

serve — real-time webhook receiver

Runs an HTTPS-ready webhook receiver. CourtListener pushes DOCKET_ALERT events to it, and each delivery processes a single entry and re-renders just the affected calendar in seconds. Bypasses the daily polling quota entirely.

uv run case-calendar serve --host 127.0.0.1 --port 8000
Flag Purpose
--host <addr> Bind address. Default 127.0.0.1.
--port <n> Bind port. Default 8000.

The receiver is HTTP, not HTTPS — put it behind Caddy / nginx / Cloudflare Tunnel for the public-facing TLS. See real-time webhooks for the full registration walk-through.

emit — force-render calendars

Re-renders every ICS file (and pushes to gcal / M365 if configured) without pulling new data. Useful after editing config.yaml or hand-fixing a row in the store.

uv run case-calendar emit

No flags. Always operates on all calendars.

setup gcal / setup m365 — one-time OAuth

Runs the interactive OAuth flow for a push backend and caches the refresh token to disk.

uv run case-calendar setup gcal
uv run case-calendar setup m365

Run once per machine. Both flows open a browser, ask you to grant permission, and write the token cache to the path configured by google_token_path / m365_token_path. Subsequent runs of sync / serve / emit auto-push to that backend with no flag.

See calendar backends for the Cloud Console / Entra app registration steps that precede these.

summarize — AI case summaries (opt-in)

Generates per-docket AI prose summaries for the index page. Gated on case_summaries.enabled: true in config.yaml. Most users won’t need to run this manually — summaries auto-refresh during sync and serve when a new primary document or disposition lands.

uv run case-calendar summarize
Flag Purpose
--case <case_id> Summarize only this one case.
--force Regenerate even when a summary row already exists. Use after a model or prompt change.
--no-emit Skip the index.html re-emit after writing.

See case summaries.

show — dump current state

Prints every hearing and deadline currently in the store, grouped by case. Read-only — useful for sanity checks.

uv run case-calendar show
uv run case-calendar show --case us-v-wang
Flag Purpose
--case <case_id> Limit to one case.

prune — clean up orphaned data

After you remove a case (or one of its dockets) from config.yaml, the hearings, deadlines, entry-fingerprint cache, and AI summaries tied to the deleted docket would otherwise live forever in the store. prune deletes them. Dry-run by default — pass --apply to actually delete.

uv run case-calendar prune          # dry-run: print what would be deleted
uv run case-calendar prune --apply  # actually delete

Back up data/case-calendar.sqlite (and its -wal / -shm sidecar files, or run PRAGMA wal_checkpoint(TRUNCATE) first) before applying.

webhook-url — print the receiver URL

Composes the URL to paste into the CourtListener webhook dashboard. Uses CASE_CALENDAR_WEBHOOK_SECRET from .env.

uv run case-calendar webhook-url --host webhook.example.com
# https://webhook.example.com/webhooks/case-calendar/<your-secret>
Flag Purpose
--host <host> Public host where the receiver is reachable. https:// is assumed unless you pass an explicit http:// URL.
--check After printing the URL, probe the receiver’s secret-gated health endpoint to verify the host is reachable, Case Calendar is the service answering, and the secret in .env matches the one the running receiver expects.

--check is the single command that catches the common deployment failure modes: a Cloudflare access policy intercepting the path, a stale Caddy config pointing at the wrong port, or a secret mismatch between the config file and the running daemon.

Global flag

Flag Purpose
-c <path>, --config <path> Use a different config file. Default config.yaml.

Environment variables

The commands above read their secrets and provider settings — COURTLISTENER_TOKEN, the *_API_KEY keys, LLM_PROVIDER / LLM_MODEL and the per-track overrides, CASE_CALENDAR_WEBHOOK_SECRET, M365_CLIENT_ID, LOG_LEVEL, and the Ollama settings — from .env in the project root, loaded automatically via python-dotenv before any module touches the environment. See Configuration → Environment variables for the complete table.