Temporal Worker: WORKERROLE deployment-safety default
Date: 2026-08-13
Type: Behavior change (operational safety)
Severity: Medium — no API change; changes what worker.py does when run outside a deployment
Shipped in: staging → main promotion #2980 (2026-08-13)
Change PR: #2975
Overview
Section titled “Overview”apps/temporal-worker/worker.py now reads a WORKER_ROLE environment variable that
selects what the process does:
WORKER_ROLE |
Serves | Creates schedules |
|---|---|---|
pdf-only (default) |
pdf-extraction only |
No |
full |
pdf-extraction + email-processing |
Yes (~10) |
The default is pdf-only. Only apps/temporal-worker/docker-entrypoint.sh sets
full, and that entrypoint is the sole launcher the deployment uses (Dockerfile
CMD; the systemd unit in infrastructure-gcp/ansible/playbooks/temporal-workers.yml
runs the image with no command override). So a deployed worker is byte-for-byte
unchanged, and a bare python worker.py on a laptop no longer registers schedules or
serves the email queue.
An unrecognised WORKER_ROLE value raises on startup rather than falling back — a
typo in a deployed .env crash-loops visibly instead of silently parking a deployed
worker in pdf-only with its schedules and email queue offline.
Running worker.py on a laptop pointed at staging registered all ~10 schedules
against the staging database and the email-pull-dispatcher schedule actually fired.
It was harmless only by luck: REDIS_URL was unset, so no connection was ever made.
Two live hazards it would otherwise have hit:
- the email-pull dispatcher polls real customer mailboxes every 3 minutes, using whatever credentials are in the environment;
- the cleanup schedules delete from the database and from the R2 bucket prod and staging share — each environment’s cleanup deletes the other environment’s uploads.
worker.py is built for a deployed environment; nothing in it previously knew that.
WORKER_ROLE makes “this is a deployment” an explicit fact the container entrypoint
asserts, rather than a heuristic the worker has to guess.
What operators need to know
Section titled “What operators need to know”-
To extract a document locally, run the default — no flag, nothing to remember:
Terminal window cd apps/temporal-workerTEMPORAL_ADDRESS=localhost:17233 uv run python worker.py -
Do not set
WORKER_ROLE=fullon a laptop to “run the real worker” without understanding that it turns on the mailbox poller and the shared-bucket cleanup jobs above. -
docker compose up temporal-workerruns the same image, so it still getsfulland reconciles schedules against the local Temporal — unchanged. If your root.envpoints at staging, that stack still polls real mailboxes.
See apps/temporal-worker/README.md (WORKER_ROLE section) for the full run guide.
Deploy verification
Section titled “Deploy verification”Both environments were confirmed live on the new code, running in full mode (the
deployed behavior is unchanged):
| Environment | VM | Container recreated | Unit image ref | Startup log |
|---|---|---|---|---|
| staging | erp-staging-temporal |
2026-08-13 12:22:40 | temporal-worker:latest-staging |
Starting Temporal workers (line 602) |
| production | erp-prod-temporal |
2026-08-13 12:44:10 | temporal-worker:latest-main |
Starting Temporal workers (line 602) |
The Starting Temporal workers line only executes on the full path; the pdf-only
path logs a WORKER_ROLE=pdf-only warning and returns before it. That line moving to
602 (from 562 on the prior code) is also the fingerprint that the running process
contains the new WORKER_ROLE block.
Rollback
Section titled “Rollback”The behavior gate is opt-out via the entrypoint. To force the old always-full behavior
without a code change, set WORKER_ROLE=full in the worker’s environment. To revert the
code, revert #2975 and redeploy;
the deployed path was unchanged, so a revert is low-risk.
Also in this promotion
Section titled “Also in this promotion”The 2026-08-13 staging → main promotion carried 13 commits. The rest are routine fixes, documented by their own PRs and changesets:
fix(webapp): server-side duplicate-order guard + WhereFour-OMseed path (#2959)fix(dagster): resolve WhereFour tier prices against the live catalog (#2971)fix(workbench): sync marimo’s direct filesystem writes to git (#2979)- chatwoot state-locking cherry-pick (#2956)
fix(infra-gcp): R2 backend native state locking, chatwoot-web Cloud Run start, dead Cloud Build trigger cleanup, staging terraform-drift reconcile;fix(ci): Terraform runner bump (#2953)chore: version packages(#2961, #2957)