---
title: "List Flywheel Runs Route"
method: GET
path: "/api/v1/voice-agents/{voice_agent_id}/flywheel-runs"
tags: ["flywheel"]
---

# List Flywheel Runs Route

`GET /api/v1/voice-agents/{voice_agent_id}/flywheel-runs`

Recent ``FlywheelRun`` rows for this agent.

Ordered by ``created_at`` DESC. ``sim_passed`` + ``promoted_at`` +
``candidate_version_id`` + ``aggregator_outcome`` on each row let
the caller compute status client-side (no N+1).

## Path parameters

- `voice_agent_id` string, uuid, required

## Query parameters

- `limit` integer — Max rows returned, ordered by created_at DESC. V0 default (50) covers typical review session; max (200) bounded to prevent large page scans.

## Response `200`

Successful Response

- FlywheelRunRead[]
  - `id` string, uuid, required
  - `tenant_id` string, uuid, required
  - `voice_agent_id` string, uuid, nullable, required
  - `voice_agent_group_id` string, uuid, nullable, required
  - `target_agent_type` string, required
  - `triggered_by_user_id` string, uuid, nullable, required
  - `candidate_voice_agent_id` string, uuid, nullable, required
  - `candidate_version_id` string, uuid, nullable, required
  - `source_version_id` string, uuid, nullable, required
  - `status` 'draft' | 'aggregating' | 'plan_ready' | 'applied' | 'sim_running' | 'sim_passed' | 'sim_failed' | 'promoted' | 'rolled_back' | 'dismissed', required — Cycle state machine for ``FlywheelRun`` (Flywheel V1.0 cockpit). Single source of truth for cockpit mode (readonly ↔ editing), promote-button gating, and rail-card vocabulary. Per V1 invariant #3 — never derived from ``aggregator_outcome``; the column is the canonical signal. ``aggregator_outcome`` remains as the V0.6/V0.7 audit detail (per V1 invariant #7 — additive over renaming); V1.9 deprecates it at the Pydantic layer. Allowed transitions (enforced in tests, not in DB): DRAFT → AGGREGATING → PLAN_READY → APPLIED → SIM_RUNNING → (SIM_PASSED | SIM_FAILED) → PROMOTED → ROLLED_BACK DRAFT | PLAN_READY | APPLIED | SIM_RUNNING | SIM_PASSED | SIM_FAILED → DISMISSED Lifecycle: - DRAFT: row created at intent-confirm; no plan yet. - AGGREGATING: aggregator running. - PLAN_READY: FDE produced a ``PromptEditPlan``; not applied. - APPLIED: candidate ``VoiceAgentVersion`` written. - SIM_RUNNING: simulator kicked off, awaiting webhook. - SIM_PASSED / SIM_FAILED: webhook reported verdict. - PROMOTED: candidate flipped to prod ``active_version_id``. - ROLLED_BACK: a prior promote was reverted in a later cycle. - DISMISSED: operator closed the cycle without promoting. Column-width note: the underlying VARCHAR is ``VARCHAR(32)`` (set in the V1.0 migration via explicit ``ALTER COLUMN ... TYPE VARCHAR(32)``), even though SQLAlchemy's ``Enum(native_enum=False)`` auto-derives ``VARCHAR(11)`` from the longest member name (``"aggregating"``). Same drift pattern as ``AggregatorOutcome`` after V0.7.1 (see migration ``d4a8b3c5e7f1``). The next engineer who runs ``alembic revision --autogenerate`` will see SQLAlchemy propose narrowing back to 11 — the column comment on ``FlywheelRun.status`` explains why the override is intentional.
  - `intent` string, nullable, required
  - `bypass_reason` string, nullable, required
  - `dismissed_reason` string, nullable, required
  - `aggregator_outcome` 'skipped_no_findings' | 'skipped_target_agent_missing' | 'skipped_empty_plan' | 'validation_failed' | 'candidate_written' | 'llm_failed' | 'entry_staging_agent_missing' | 'bluejay_dev_agent_not_configured' | 'bluejay_simulation_not_configured' | 'candidate_not_active' | 'bluejay_api_failed' | 'apply_validation_failed', required — Terminal state of a ``FlywheelRun`` (Flywheel V0.6 + V0.7). V0.6 defined six mutually exclusive values covering every way the aggregator itself can end. V0.7 adds four more that follow the ``CANDIDATE_WRITTEN`` branch when sim kickoff fires and either fails preflight or fails the Bluejay HTTP call. The column on ``flywheel_runs`` is nullable — the row is inserted BEFORE the aggregator runs (so a mid-pipeline crash leaves an inspectable row with ``aggregator_outcome IS NULL``), and the outcome is set at each exit path. V0.7 contract: when any of the four new codes lands, the run's ``candidate_version_id`` is still populated (V0.6's aggregator wrote the candidate before sim kickoff ran). The row is self-documenting as "V0.6 wrote the candidate, V0.7 couldn't sim-test it, see the outcome code for why." V0.8's promote gate separately blocks on ``sim_passed IS NOT True``. V0.6 codes: - SKIPPED_NO_FINDINGS: zero open reviewer findings for the target agent at run time. No LLM call, no version write. - SKIPPED_TARGET_AGENT_MISSING: the target agent could not be resolved — missing, wrong tenant, or non-production. No LLM call, no version write. (Named for a clone until that concept was removed; the aggregator now targets the production agent.) - SKIPPED_EMPTY_PLAN: LLM ran cleanly and returned ``PromptEditPlan(edits=[])`` — its verdict was "none of these findings merit an edit." Distinct from LLM_FAILED (normal no-op, not a Sentry-worthy error; breadcrumb only). Findings stay open for the next run. - VALIDATION_FAILED: the non-empty plan failed pre-validation (unknown finding_id / scenario_id / preset key) or post-apply validation (pydantic ``ScenarioBase.model_validate`` / prompt delimiter check / tool-name collision). All-or-nothing — no version written. Cited findings transition to ``dismissed`` with reason ``"ai_suggestion_invalid"``; uncited stay open. Sentry captures because this signals AI drift or validator drift. - CANDIDATE_WRITTEN: success. New ``VoiceAgentVersion`` on the target agent with ``source=AI_SUGGESTION`` + ``flywheel_run_id=<run.id>`` + ``suggestion_rationale=<plan>``. Cited findings transition to ``batched``; uncited stay open (cited-only batching — uncited findings aren't addressed and deserve another run). V0.7 additionally populates ``sim_started_at`` and ``bluejay_sim_external_id`` on this outcome — the sim is waiting for the webhook to flip ``sim_passed``. - LLM_FAILED: pydantic-ai agent raised (network error, malformed response, timeout). Findings stay open (retryable next run). Sentry captures. V0.7 sim-kickoff codes (all follow a successful candidate write, all leave ``sim_started_at`` and ``bluejay_sim_external_id`` NULL so the webhook handler can't match a stale sim): - ENTRY_STAGING_AGENT_MISSING: the source call's entry agent could not be resolved when the sim was kicked off (e.g. deleted after V0.6's aggregator ran). - BLUEJAY_DEV_AGENT_NOT_CONFIGURED: the agent exists but its ``bluejay_dev_agent_external_id`` is NULL. Operator pastes the id via ``PATCH /voice-agents/{id}/bluejay-dev-agent-id`` (V0.3 endpoint), re-runs flywheel. - BLUEJAY_SIMULATION_NOT_CONFIGURED (V0.7.1): the agent exists and has a Bluejay Dev Agent id, but its ``bluejay_simulation_external_id`` is NULL. Bluejay's real API has a two-level shape (Agent + Simulation template), so the operator creates a Simulation in Bluejay's UI targeting that Dev Agent once and pastes the simulation id via ``PATCH /voice-agents/{id}/bluejay-simulation-id``. Mirrors the V0.3 Dev-Agent-id paste pattern. - CANDIDATE_NOT_ACTIVE: defensive. V0.6's aggregator writes the candidate and flips the agent's ``active_version_id`` atomically in the same transaction, so this firing indicates a real bug — Sentry captures. Never expected in steady state. - BLUEJAY_API_FAILED: Bluejay HTTP timeout / 5xx / network error on either ``create_digital_human_from_transcript`` or ``queue_simulation_run``. Distinct from LLM_FAILED (V0.6's LLM engineer path) so the FlywheelRun audit trail stays crisp. V1.1a apply-time code: - APPLY_VALIDATION_FAILED: ``apply-plan`` re-validated the stored plan against the prod agent's current ``metadata_json`` and ``apply_and_validate`` returned validation errors — the plan was built against stale agent state (manual edit landed between preview and apply, or the mutator now hits a state the validator rejects). The route returns 422; the run transitions to ``DISMISSED`` with this outcome and ``validation_errors`` populated. Distinct from ``VALIDATION_FAILED`` (V0.6 — validation failed at preview time, before the candidate could be written) because the operator's remediation differs: re-run preview to get a fresh plan against the current state. Findings stay open.
  - `finding_ids` string[], required
  - `prompt_edit_plan` object, nullable, required
  - `llm_error` string, nullable, required
  - `validation_errors` object[], nullable, required
  - `sim_passed` boolean, nullable, required
  - `sim_started_at` string, date-time, nullable, required
  - `sim_completed_at` string, date-time, nullable, required
  - `bluejay_sim_external_id` string, nullable, required
  - `sim_provider` string, nullable, required
  - `sim_coverage_note` string, nullable, required
  - `sim_scenarios` object, nullable, required
  - `promoted_at` string, date-time, nullable, required
  - `promoted_version_id` string, uuid, nullable, required
  - `candidate_version_number` integer, nullable
  - `open_findings_count` integer, nullable
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required

## Other responses

- `400` — Target voice agent is not eligible (not found in this tenant / target is a staging clone rather than production)
- `422` — Validation Error

## Changes

> 19 revisions in range; 1 not diffed.

- **2026-09-03** `d65b8fefae34` — 2 info
  - added the optional property `detail/items/ctx` to the response with the `422` status
  - added the optional property `detail/items/input` to the response with the `422` status

[Change history](https://skmtc.dev/getanana/apis/cleon-api/changes/api/v1/voice-agents/:voice_agent_id/flywheel-runs/get.md)

---

[API](https://skmtc.dev/getanana/apis/cleon-api.md) · [All operations](https://skmtc.dev/getanana/apis/cleon-api/llms.txt) · [OpenAPI document](https://skmtc.dev/getanana/apis/cleon-api/revisions/34f2d9e81b0b?raw)
