Web-Enrichment Operations¶
How to bring up, run, inspect, and repair web enrichment (#842). The design is in ADR-0029 and the architecture; the pass itself is in the company pass reference.
What is deployed¶
| Resource | Kind | Purpose |
|---|---|---|
web-enrichment |
Cloud Run service, 2 vCPU, 4 GiB, 8 requests per instance, up to 10 instances, 900 s timeout | Request API, task delivery, sweep |
web-enrich-batch |
Cloud Tasks queue, 4 dispatches/s, 40 at a time | Runs of batch requests |
web-enrich-ad-hoc |
Cloud Tasks queue, 4 dispatches/s, 12 at a time | Runs of ad hoc requests |
web-enrich-sweep |
Cloud Scheduler, every 10 minutes | Re-enqueue lost or stale runs |
web-starter |
Cloud Run job, on demand | Operator caller of the request API |
All are in infra/web_service.tf. The queues retry a failed delivery up to
five times with 30 to 600 seconds backoff; the run itself stops after three
attempts.
The service runs as web-enrichment-<env>@<project> and reads six secrets:
cvr-database-url (as DATABASE_URL), webshare-proxy-url,
openrouter-api-key, openai-api-key, dataforseo-login, and
dataforseo-password. The starter runs as web-starter-<env>@<project> and
reads no secret. Raw evidence goes to the data bucket under raw/web/; the
service may create and read objects there and nowhere else.
No service allows unauthenticated calls. Cloud Tasks and Cloud Scheduler
present the service's own identity with the task audience
https://web-enrichment-tasks.<project>.<env>; backends and the starter
present the request audience https://company-requests.<project>.<env>.
Permitted backend accounts are listed in web_request_callers.
Bring-up¶
OpenTofu creates the service identities, the secret containers, the secret
grants, and the service identity's iam.serviceAccountUser on itself, which
it needs to mint task tokens (infra/access.tf, ADR-0030). Only the secret
values are set out of band; the service is not created before they exist:
cd infra
CVR_ES_USER="$CVR_ES_USER" \
CVR_ES_PASSWORD="$CVR_ES_PASSWORD" \
DATABASE_URL="$DATABASE_URL_TEST" \
WEBSHARE_PROXY_URL="$WEBSHARE_PROXY_URL" \
OPENROUTER_API_KEY="$OPENROUTER_API_KEY" \
OPENAI_API_KEY="$OPENAI_API_KEY" \
DATAFORSEO_LOGIN="$DATAFORSEO_LOGIN" \
DATAFORSEO_PASSWORD="$DATAFORSEO_PASSWORD" \
bash set-secrets.sh sourceagent-data-test
CD on main owns the image, the apply, and the service rollout.
Never run tofu apply or docker push from a local shell.
Submit, inspect, re-extract¶
gcloud run jobs execute web-starter --region=europe-west1 \
--project=sourceagent-data-test \
--args="-m,data_sourceagent.runtime.web_starter,refresh,--request-key,2026-09-review,--cvrs,25140199,25484550"
gcloud run jobs execute web-starter --region=europe-west1 \
--project=sourceagent-data-test \
--args="-m,data_sourceagent.runtime.web_starter,inspect,--request-id,<request-id>"
refresh takes --cvrs and optional --scenario ad_hoc. reextract takes
--cvrs and --reason; it reads each company's latest sealed bundle and AI
Mode answer again and buys no search or page. Only bundles in the current
format (bundle_version 3) are read; a company whose latest bundle is
older is ineligible (no-retained-evidence) and needs a refresh. inspect prints the status,
the outcome counts, and one line per company; --details prints the full
step log of each run.
The HTTP contract is POST /company-requests with an Idempotency-Key
header and {"mode": "refresh" | "reextract", "cvrs": [...], "scenario":
"batch" | "ad_hoc", "reason": "..."}, answered by HTTP 202, a Location
header, and the receipt. The same key with the same intent returns the first
receipt; with another intent it returns 409. An invalid intent returns 422.
GET /company-requests/{request_id}[?details=true] returns the progress of
the caller's own request.
A member of a receipt is created (with its run ID), already-running (the
open run of that company, reused), or ineligible (unknown-company,
inactive-company, or no-retained-evidence for a re-extraction).
Read one company's story¶
select status, outcome, terminal_reason, attempts, started_at, terminal_at,
log -> 'costs' as costs, log -> 'counts' as counts
from company_enrichment_run where cvr = 10049385 order by id desc limit 1;
select step ->> 'step' as step, step
from company_enrichment_run, jsonb_array_elements(log -> 'steps') as step
where id = <run_id>;
The steps show every search, every fetch with its attempts (HTTP, plain HTTP, browser, status, error), each lead list, each ownership decision with the signals it saw, and the reading results with refused quotes.
Repair and alerts¶
The sweep runs every ten minutes. It ends runs that used three attempts as
failed/attempts-exhausted, and it enqueues runs that are queued and
untouched for ten minutes or claimed for more than twenty minutes. It logs
nothing when there is nothing to do.
| Alert | Fires when | First action |
|---|---|---|
<env> web_enrichment 5xx |
The service returned 5xx (a failed delivery) | Read the service logs for web run failed and the run's log.last_error |
<env> web runs failed |
A run ended as failed |
Read the run's terminal_reason and log |
To retry one company, submit a new request for it. A run that is still open is reused, not duplicated.
Costs¶
Search and AI Mode cost about USD 0.004 to 0.006 per company. The model calls
(z-ai/glm-5.3-flash by default, WEB_LLM_MODEL to change it) and the
embedding cost fractions of a cent. The run log records the confirmed search
and model cost per run. Proxy and Cloud Run costs are not included.