Background jobs
The API is not only request/response. Eleven workers run inside the same process: they keep watchlists current, collect adverse media, re-screen monitored subjects, deliver webhooks and age out data. This page states when each runs, which are off by default, and what you can control at runtime.
Several jobs are OFF until you switch them on. That is deliberate and marked per job below. A job you believe is running and is not is worse than one you know is off.
What runs, and when
| Job | Runs | Default | Config key |
|---|---|---|---|
| Watchlist ingestion | scheduled + on demand | on | Watchlists:* |
| Adverse-media prefetch (DOC) | every 15 min, 1 subject / 60 s | off | AdverseMedia:Prefetch:Enabled |
| Adverse-media slice tail (GKG) | every 15 min, follows the feed | off | AdverseMedia:SliceTail:Enabled |
| Scheduled re-screen | poll 60 min · daily 24 h · full 30 d | off | Monitoring:Enabled |
| Batch processing | continuous, leases queued batches | off | Batch:Enabled |
| Coverage audit | every 12 h (first run 2 min after start) | on | Coverage:* |
| Retention purge | every IntervalHours | off | Retention:Enabled |
| Webhook capture purge | hourly, 24 h TTL | always on | — |
| Webhook outbox delivery | continuous; retries 5 m → 1 h → 5 h → 18 h | on | Webhooks:* |
| Triggered job runner | polls 5 s, heartbeat 30 s | on | — |
| Warm-up | once at startup | on | — |
Two to check before going live
Retention purge is off by default.Retention:Enableddefaults tofalse, so nothing ages out screening records until you set it. Adverse-media evidence is criminal-offence data about identifiable people (GDPR Article 10) — decide your retention period and enable this deliberately.
Adverse media is off by default, and a miss is not a pass. With
prefetch and slice-tail disabled an adverse-media screen returns Unavailable,
never “clean”. That is the honest answer — we did not check —
and it must not be read as nothing found.
The webhook capture purge has no off switch, on purpose. It deletes captured payloads after 24 hours, and those payloads contain screening results. A configurable purge is one somebody eventually disables, turning a debugging convenience into an indefinite store of personal data.
Starting a job on demand
Long-running jobs start without a restart. Every trigger is single-flight: a
second request while one is running returns 409 Conflict rather than starting a
duplicate.
| Endpoint | Starts |
|---|---|
POST /v1/admin/watchlists/ingest | Full watchlist re-ingest |
POST /v1/admin/jobs/retag | Re-tag in place (no re-fetch) |
POST /v1/admin/jobs/coverage-audit | Coverage audit |
| Status | Meaning |
|---|---|
202 Accepted | Queued — poll the status endpoint |
409 Conflict | Already running. The single-flight guard, not an error |
403 Forbidden | Your account lacks platform_operator |
429 Too Many Requests | Trigger rate limit — Retry-After says when |
Triggering requires theplatform_operatorrole. It is deployment-wide and deliberately not implied byadmin, so a tenant administrator cannot start a job that affects every tenant.
Observing
| Endpoint | Shows |
|---|---|
GET /v1/jobs/status | Running jobs with progress |
GET /v1/ingest/history | Past runs: when, duration, outcome, counts |
GET /v1/coverage | Live PEP coverage matrix and when it was measured |
A job that is off shows as off — the status endpoint does not invent a schedule for a disabled worker. Trust it over the configuration file you edited: setting a key in a manifest is not the same as it reaching the process.
Behaviour under failure
- Single-flight is enforced in the database, not in memory: a partial unique index allows one queued/running row per job name, so two instances cannot both start it. In-process locks would not survive scale-out.
- A job whose process dies is resumed, not orphaned. Long jobs hold a lease and heartbeat it every 30 s; a lease that stops beating is reclaimed and work continues from its cursor.
- Webhook delivery is a durable outbox, off the request path. A screen never
waits on your endpoint. Retries run 5 m → 1 h → 5 h → 18 h, each carrying the
same
eventIdso you can deduplicate. - Rate limits back off rather than spin. The adverse-media collectors treat a 429 as a cooldown (300 s, growing to a 1800 s cap).
Configuration
{
"AdverseMedia": {
"Prefetch": {
"Enabled": false,
"PollMinutes": 15,
"DelaySecondsBetweenSubjects": 60,
"MaxSubjectsPerRun": 500
},
"SliceTail": { "Enabled": false }
},
"Monitoring": { "Enabled": false, "PollIntervalMinutes": 60,
"DailyIntervalHours": 24, "FullIntervalDays": 30 },
"Retention": { "Enabled": false, "DefaultDays": 1825,
"IntervalHours": 24, "BatchSize": 500 },
"Batch": { "Enabled": false },
"Coverage": { "IntervalHours": 12 }
}
Every key is settable by environment variable using the standard double-underscore form:
AdverseMedia__Prefetch__Enabled=true
Retention__Enabled=true
Monitoring__Enabled=true
Resource profile
All eleven run in the API process. There is no separate worker, no scheduler service and no message broker to operate.
| Job | Cost |
|---|---|
| Slice tail | ~15 MB per 15 min; parsing is ~0.018% of one core |
| Prefetch | one outbound call per minute while enabled |
| Coverage audit | a few queries, twice a day |
| Retention purge | BatchSize rows per pass, throttled |
| Triggered job runner | one lightweight poll every 5 s |
The heavy job is a full watchlist ingest — which is why it is on-demand and single-flight rather than frequent.