SysEngineering

Greg’s weekly Google Workspace digest

A Cloud Run Job that posts one message a week to a private Slack channel: what changed across every connected Google Workspace organization, sorted by how loudly it should be saying it.

It reuses workspace.py’s per-tenant delegated credentials, so it needs no new scopes and no new Admin Console work — if Greg can answer questions about a workspace, the digest already covers it. It is as read-only as the assistant is.

Why it exists

Greg answers well when someone remembers to ask. The most valuable thing it found in its first week — an organization with zero 2SV enrollment — surfaced by accident, as a footnote to a question about licenses. The digest flips that around: the report arrives whether or not anyone thought to ask.

What a message looks like

Google Workspace weekly digest - 2026-09-04
<one-sentence headline>
30/30 workspaces  |  2,925 users  |  2,074/2,248 licenses (174 free)

:red_circle: Needs attention now
- 8 super admins without 2-Step Verification in 6 orgs
   - Schmidt Entity Sandbox: jfrum@..., jjaquez@..., jjimenez@...

:credit_card: Licenses
- Out of licenses (5): The Schmidt (182/182), Ocean Hour Farm (72/72), ...
- Nearly out (15): AGOG (3), Rise for the World (2), ...
- Reclaimable: 174 suspended accounts still holding licenses ...
- Fleet: 2,074/2,248 in use, 174 free

:large_orange_circle: Heads up
- New third-party apps authorized
- No 2SV enforcement policy in 17 orgs (1,302 accounts) - unchanged from last week

:white_circle: Trends
- 943 dormant accounts (no sign-in in 90 days) and 125 never signed in

Sections and tiers are different things

Tier is severity and decides whether anyone is @-mentioned: red and amber findings ping, info never does. Section is layout — what sits next to what. Licenses are amber (so they still ping) but display in their own block, because that is what the IT team acts on most weeks and it should not be hunted for inside a list of security items.

Section Contents
:red_circle: Needs attention now Super admin without 2SV, new super admin, 2SV enforcement policy removed, all SSO assignments removed, Google-flagged suspicious logins, a workspace that failed to report
:credit_card: Licenses Out of licenses, nearly out, reclaimable suspended seats, fleet total. Always shown, even on a clean week
:large_orange_circle: Heads up New third-party OAuth app, new domain, no 2SV enforcement policy
:white_circle: Trends Dormant and never-signed-in accounts

If nothing is red, the first section becomes a green “Nothing needs attention now” rather than vanishing, so a clean week reads as clean instead of broken.

Design decisions that matter

These are the things that will look arbitrary later and are not.

Configuration

Non-secret config lives in digest.env.yaml; secrets come from Secret Manager.

Variable Purpose
DIGEST_SLACK_CHANNEL Channel id to post into (required unless dry run).
DIGEST_MENTION_USER_IDS Space/comma separated member ids, @-mentioned only on red/amber.
DIGEST_STATE_BUCKET / DIGEST_STATE_OBJECT GCS location of the snapshot.
DIGEST_LICENSE_WARN_FREE Free seats at or below this is “nearly out”. Default 3.
DIGEST_DORMANT_DAYS Dormancy threshold in days. Default 90. Set 0 to skip.
DIGEST_DRY_RUN Print instead of posting; writes no snapshot.
DIGEST_HEADLINE Set false to skip the model-written headline.
GW_EXCLUDED_WORKSPACES Shared with the service; excluded tenants are not reported.

First-time setup

gcloud storage buckets create gs://gw-admin-assistant-digest-state `
  --project gw-admin-assistant --location us-central1 `
  --uniform-bucket-level-access

gcloud storage buckets add-iam-policy-binding gs://gw-admin-assistant-digest-state `
  --project gw-admin-assistant `
  --member serviceAccount:1039144753775-compute@developer.gserviceaccount.com `
  --role roles/storage.objectAdmin

Then invite Greg to the private channel and put its id in digest.env.yaml.

Deploy

gcloud run jobs deploy gw-weekly-digest `
  --project gw-admin-assistant --region us-central1 `
  --source . `
  --command python --args digest_job.py `
  --task-timeout 30m --max-retries 1 `
  --service-account 1039144753775-compute@developer.gserviceaccount.com `
  --set-secrets SLACK_BOT_TOKEN=gw-assistant-prod-slack-bot-token:latest,ANTHROPIC_API_KEY=gw-assistant-anthropic-api-key:latest `
  --env-vars-file digest.env.yaml

Dry run first — prints the digest, posts nothing, writes no snapshot:

gcloud run jobs execute gw-weekly-digest --project gw-admin-assistant `
  --region us-central1 --wait --update-env-vars DIGEST_DRY_RUN=true

Read it back out of the logs:

gcloud logging read 'resource.type="cloud_run_job" labels."run.googleapis.com/execution_name"="<execution>"' `
  --project gw-admin-assistant --limit 300 --format "value(textPayload)"

Then a real run with DIGEST_DRY_RUN=false.

Schedule

Monday 10:00 Pacific, so it lands while the IT director is at his desk. Cloud Scheduler handles PST/PDT itself, so it does not drift an hour in summer.

gcloud scheduler jobs create http gw-weekly-digest-monday `
  --project gw-admin-assistant --location us-central1 `
  --schedule "0 10 * * 1" --time-zone "America/Los_Angeles" `
  --uri "https://us-central1-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/gw-admin-assistant/jobs/gw-weekly-digest:run" `
  --http-method POST `
  --oauth-service-account-email 1039144753775-compute@developer.gserviceaccount.com

Tests

.\.venv\Scripts\python.exe -m unittest test_digest_job -v

36 tests: license parsing across SKUs, every finding kind, the grouping guarantees, chronic-unchanged marking, headline grounding, OAuth accumulation, mention-only-on-red/amber, the green all-clear, snapshot round-tripping, the usage-report walk-back past unpublished days, and the compactness guard.

Known gaps