Skip to content

Run Lifecycle Reference

Every execution on QubitHub is a Run row: a database record that tracks one circuit-execution attempt from submission to terminal state. This page documents the lifecycle so you can answer “what’s happening to my run right now?” and (if you’re a privacy-aware user) “what does QubitHub record about my executions?”.

A run moves through these states:

pending → running → completed
→ failed
→ cancelled
StateWhat it means
pendingRun was created (POST /runs returned 201) but the executor hasn’t picked it up yet
runningThe Dagster executor has the run; the circuit is being prepared or executed in the sandbox
completedTerminal — execution succeeded and results were written. Stays in this state forever
failedTerminal — execution failed at some stage. error_class + error_message carry the cause. Stays in this state forever
cancelledTerminal — the user cancelled the run before it reached a terminal state

Each transition is recorded in the audit log and emits a server-side analytics event. Both happen in the same database transaction as the state change, so analytics and audit are guaranteed consistent with the actual run state.

State transitions write to the audit_logs table via the platform’s log_run_transition helper. The audited actions are:

ActionFires on
run.startedpending → running
run.completedrunning → completed
run.failedrunning → failed (covers both execute-stage and storage-stage failures)

Each audit row carries:

  • actor_user_id — who owns the run (server-side, not client-claimed)
  • action — one of the three above
  • resource_type = "run", resource_id = <run UUID>
  • metadata — JSON blob with the transition-specific context (framework, backend, error class on failures, etc.)
  • created_at — server-side timestamp

The audit log is the source of truth for reconstructing what happened to a stalled or surprising run — gh query the run ID, get the full transition timeline.

A parallel set of events fires via the platform’s services/posthog_server.py capture helper. These are canonical for operational reliability dashboards (completion rate, framework-level error rate, queue-depth proxies) — distinct from the user-action funnel which still uses the client-side circuit_executed event.

EventFires onStage
run_requestedPOST /runs returns 201Submission
run_startedexecute_circuit_op flips status to runningExecute begins
run_completedstore_results_op writes results and flips status to completedTerminal — success
run_failedAny op flips status to failedTerminal — failure

Each event carries run_id, circuit_id, framework, backend_class / backend_name, and stage-specific fields like execution_time, shots, unique_states (completed), or error_class, stage (failed).

run_viewed_shared_link is a separate client-side event that fires when a run-detail route mounts — dedup’d per runId so refreshes don’t inflate.

For the full canonical event schema, see docs/architecture/POSTHOG_EVENTS.md in the repository.

Analytics capture is gated on your consent at submission time:

  1. The frontend sends X-Analytics-Consent: granted|denied on POST /runs, defaulting to denied unless the cookie-consent banner has been accepted.
  2. The router stamps the decision onto the run row as Run.analytics_consent (Boolean, nullable for legacy runs).
  3. The background Dagster ops read Run.analytics_consent and short-circuit the PostHog capture if it’s not True.

This means: revoking consent on the cookie banner stops new runs from being captured — old runs that were captured under prior consent remain in PostHog (you can request deletion via support@qubithub.co under GDPR Article 17). Per-run consent stamping is on the GDPR-compliant default-deny side per EDPB Guidelines 5/2020.

The audit log is not consent-gated — it’s the platform’s record of what happened to its own resources, kept for security and debugging. It records actor_user_id (your account) but not client telemetry like browser fingerprint or IP.

A run row contains:

FieldWhat it is
idRun UUID — the public URL slug at /<owner>/<circuit>/runs/<id>
user_idThe user who submitted (always your account, even on someone else’s public circuit)
circuit_id + circuit_versionWhat was executed
statusCurrent state (see above)
backend_type / backend_nameWhat you asked for
frameworkResolved from the circuit’s manifest
shotsNumber of repetitions requested
resultsOutcome counts, execution_time, unique_states, etc. (only populated on completed)
error_class + error_messagePopulated on failed. Class is one of six taxonomy values; see Troubleshooting → Run failure error classes
analytics_consentTrue / False / NULL (legacy) — controls server-side capture only
created_at, started_at, completed_atServer-side timestamps

Public-circuit runs can be viewed by anyone with the URL (the run page does not expose the submitter’s identity to anonymous visitors beyond what’s on the run-page badges). Private-circuit runs are 404 to anyone except the circuit owner.

If a run sits in running longer than expected:

  1. Check the run page — the badge shows the current state and the framework / backend being used
  2. The dagster-stranded-runs health monitor at /api/health/dagster/runs reports counts of runs that have been running past their expected window. Public endpoint, returns JSON.
  3. If a run is stuck and never reaches a terminal state, Submit Feedback with the run URL — the audit log + Sentry trace are keyed by run ID and let us reconstruct the failure path

The platform also runs a recovery sweep that catches runs stranded past their TTL and flips them to failed with a synthetic internal_error class. So a truly orphaned run is a bug, not a forever-stuck state.