---
title: "Preview Plan Agent Route"
method: POST
path: "/api/v1/voice-agents/{voice_agent_id}/preview-plan"
tags: ["flywheel"]
deprecated: true
---

# Preview Plan Agent Route

`POST /api/v1/voice-agents/{voice_agent_id}/preview-plan`

> **Deprecated.**

Flywheel V1.1a — start a cycle on a standalone voice agent.

Creates a new ``FlywheelRun`` (status=DRAFT, intent populated) and
its cycle-scoped ``FDEConversation``, transitions to
``AGGREGATING``, runs the aggregator's ``build_plan`` step, and
returns either a ``plan_ready`` response with proposed edits OR a
``dismissed`` response with empty plans (no findings, empty plan,
LLM failure, or validation failure — distinguished by
``aggregator_outcome`` on the run row, queryable via
``GET /flywheel-runs``).

## Path parameters

- `voice_agent_id` string, uuid, required

## Request body

- PreviewPlanRequest — V1.1a — body of ``POST .../preview-plan``. ``intent`` is the operator-supplied free-text describing why this cycle is being started. Persisted on ``FlywheelRun.intent`` (varchar 500); the intent confirm card is the typical caller. The endpoint stores it verbatim — no formatting, no truncation beyond the column width.
  - `intent` string, required — Operator-supplied free-text intent for the cycle. Max 500 chars (matches FlywheelRun.intent column width). Empty string accepted (rare; V1.5b cold-open path).

## Response `200`

Successful Response

- PreviewPlanResponse — V1.1a ``POST /preview-plan`` response. Always populated regardless of whether ``build_plan`` reached ``PLAN_READY`` or short-circuited to ``DISMISSED``: - On success (``status="plan_ready"``): ``plans`` carries the proposed edits; ``findings_cited`` carries the union of finding IDs cited across edits. - On dismissal (``status="dismissed"``): ``plans=[]`` and ``findings_cited=[]``. ``run_id`` and ``conversation_id`` are always populated so the cockpit chat has a conversation to stream into; the FDE's "nothing to propose" card renders into the cycle conversation.
  - `run_id` string, uuid, required
  - `conversation_id` string, uuid, required
  - `subject` SubjectRead, required — Subject of a Flywheel cycle (V1.1a). Mirrors the ``Subject`` discriminated dataclass used internally — a cycle is scoped to either a standalone ``VoiceAgent`` or a ``VoiceAgentGroup``. The XOR is enforced at the DB layer via ``ck_flywheel_runs_subject_xor`` on ``flywheel_runs``.
    - `kind` 'agent' | 'group', required
    - `id` string, uuid, 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.
  - `plans` PlanEntry[], required
    - `voice_agent_id` string, uuid, required
    - `plan` PromptEditPlan, required — The LLM engineer's top-level output: a list of edits + rationale. An empty ``edits`` list is a valid answer — "none of these findings merit an edit." The aggregator treats empty plans as ``SKIPPED_EMPTY_PLAN`` (not ``LLM_FAILED``), leaves findings open, and persists the plan (overall_rationale included) for debugging. ``overall_rationale`` is always required — on the empty-plan path it's the LLM's explanation of why no edit was warranted; on the non-empty path it's an overview the promotion reviewer reads before approving the candidate.
      - `edits` union[], required
        - union
          - EditBaseInstructions — Overwrite the agent's top-level base-prompt content. Targets ``metadata_json["_cached_update_request"]["system_prompt"]["base_prompt"]["content"]`` — the freeform instructions that apply regardless of active scenario.
            - `kind` 'edit_base_instructions', required
            - `new_instructions` string, required
            - `addresses_finding_ids` string[], required
            - `rationale` string, required
          - EditScenarioInstruction — Overwrite a single string-typed instruction field on a scenario. Three string fields on ``ScenarioBase`` are editable (see ``voice_ai/public/scenario_models.py:447, 467, 474``). ``tool_messages`` is deliberately excluded — it's ``ToolMessageConfig | None`` (a structured object, not a string). V0.6 keeps edits minimal; a V1 ``EditToolMessages`` variant can land when finding volume justifies it. Prompt-delimiter safety: ``new_text`` MUST NOT contain ``##`` or newline characters. ``ScenarioBase``'s field validators at ``scenario_models.py:498-532`` reject both; ``apply_and_validate`` catches violations as ``validation_failed``. The system prompt tells the LLM about this constraint to reduce retry loops.
            - `kind` 'edit_scenario_instruction', required
            - `scenario_id` string, uuid, required
            - `field` 'pre_execution_instruction' | 'post_execution_instruction' | 'condition', required
            - `new_text` string, required
            - `addresses_finding_ids` string[], required
            - `rationale` string, required
          - AdjustPreset — Merge structured updates into a scenario's ``ParameterPreset``. ``preset_updates`` is a dict whose keys are restricted to the four allowed ``ParameterPreset`` subfields: ``defaults``, ``fixed``, ``required``, ``allowed``. Any other top-level key is rejected by ``apply_and_validate`` as a hallucination (prevents an LLM from inventing preset sections that don't exist in the schema). Values for each subfield are shallow-merged into the existing preset — i.e. ``preset.defaults.update(preset_updates["defaults"])`` — so individual keys can be overridden without clobbering the full dict. ``ParameterPreset``'s own pydantic validators (e.g. "``allowed`` must not conflict with ``fixed``") fire during the post-apply ``ScenarioBase.model_validate`` step.
            - `kind` 'adjust_preset', required
            - `scenario_id` string, uuid, required
            - `preset_updates` object, required
            - `addresses_finding_ids` string[], required
            - `rationale` string, required
      - `overall_rationale` string, required
  - `findings_cited` string[], required

## Other responses

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

## Changes

> 22 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/preview-plan/post.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/7a2c09378911?raw)
