Skip to content

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.