Background runs and live progress (SSE)
Connecting an ad account, „Jetzt abgleichen“, uploading or retrying conversions, applying a preset, rescoring, imports, the demo seed and a test report all start work in the background. Each start creates a run that the dashboard shows at the object that started it:
- „In Warteschlange · startet gleich“ → „Läuft · 2 von 5 Zeiträumen“ → „Fertig · 14:05“, or „Fehlgeschlagen · Google-Zugang abgelaufen · Erneut versuchen“ with an error id,
- the button stays disabled while the run is queued or running,
- „Verzögert“ appears only when a run is really overdue: queued for more than 2 minutes without a start, or running without a heartbeat for 2 minutes.
Runs start right away; the dispatcher job runs.dispatch (every minute) and the cron tick are only the
fallback for a start that got lost. Starting the same work twice returns the active run (one active run
per organization, project, kind and subject) – a double click never queues anything twice.
The run object
id, kind, projectId, subject (type, id, label), status (queued · running · done ·
failed · canceled), progress (done, total, phase – what is counted), message, error
(code, message, requestId), result, attempts, createdBy, createdAt, startedAt,
heartbeatAt, finishedAt, overdue and statusText (the line above in both languages, in the
project's time zone).
Kinds: ads.backfill, ads.sync_now, conversions.upload, conversions.retry, presets.apply,
relevance.rescore, leads.revenue_import, gsc.sync, demo.seed, reports.test_send.
Endpoints
| Call | Scope | What |
|---|---|---|
GET /v1/projects/{project}/runs | analytics:read | newest first; active=true, kind, subject_type, subject_id, limit, cursor |
GET /v1/orgs/{org}/runs | analytics:read | organization-level runs and those of its projects |
GET /v1/runs/{id} | analytics:read on its project or organization | one run |
POST /v1/runs/{id}/cancel | config:write or ads:write, or the person who started it | a queued run stops at once, a running one at its next checkpoint; finished runs answer 409 |
GET /v1/projects/{project}/runs/events | analytics:read | server-sent events (below) |
POST /v1/projects/{project}/runs | per kind (below) | start a project run: { "kind": "gsc.sync" }, relevance.rescore, demo.seed – answers { run, created } |
POST /v1/orgs/{org}/runs | config:write + project:admin on every project | start an organization run: presets.apply with input: { presetId, version, projectIds, recompute } |
MCP: runs_list, runs_get.
Starting a run
Only the kinds with a trigger in the dashboard can be started directly; the others start as part of
another action (connecting an account, uploading a conversion …) and answer 422 here.
| Kind | Level | Scope | Input | Progress counts |
|---|---|---|---|---|
gsc.sync | project | project:admin | { from?, to? } (YYYY-MM-DD) | days („12 von 30 Tagen“) |
relevance.rescore | project | config:write | – | visits („1.200 von 8.000 Besuchen“) |
demo.seed | project | config:write, demo projects only | – | days, data up to now − 15 minutes |
presets.apply | organization | config:write + project:admin on every listed project | { presetId, version, projectIds[], recompute: none·30d·90d } | projects („3 von 7 Projekten“), the result lists the outcome per project |
A second start while the same subject has a queued or running run returns that run with
created: false. A run that stopped reporting for 2 minutes is taken over by the next start right away.
Long runs store a checkpoint and continue in a fresh invocation, so a retry never repeats finished work.
Server-sent events
GET /v1/projects/{project}/runs/events?active=true
Accept: text/event-stream
Authorization: Bearer x3s_…The stream sends event: ready, then every active run (and runs finished in the last 2 minutes) once,
then event: run with the run as JSON on every change. Each event has id: <run id>:<updated ms> –
after a reconnect, send it back as Last-Event-ID and you only get newer changes. A comment line every
15 seconds keeps proxies from closing the connection. After 55 seconds the server sends event: bye
and closes (serverless time limit); reconnect while something is still running. Without SSE (or when
it fails twice), poll GET …/runs?active=true every 2 seconds, backing off to 5 seconds, and stop when
nothing is queued or running.