# Procedural Flow Assist — AI Chat Builder Support > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Make the Flow Assist AI chat builder correctly generate procedural/maintenance flows using the flat steps-array schema instead of the troubleshooting decision-tree schema. **Architecture:** The AI chat service (`ai_chat_service.py`) currently has hardcoded troubleshooting-specific prompts (schema, interview protocol, response format). We add parallel procedural versions and dispatch based on `flow_type`. The AI validator gets a procedural counterpart. The frontend gets a procedural steps preview component and the store/page handle the different data shape. **Tech Stack:** Python/FastAPI (backend), React/TypeScript (frontend), Zustand (state), Tailwind CSS (styling) --- ## Context: Procedural vs Troubleshooting Structure **Troubleshooting** flows use a recursive tree: `{ id, type: "decision", question, options, children: [...] }` — branching paths ending in solution nodes. **Procedural** flows use a flat ordered array: `{ steps: [{ id, type, title, ... }, ...] }` — sequential steps with a `procedure_end` as the final step. ### Procedural Step Schema ```json { "id": "unique-slug", "type": "procedure_step | procedure_end | section_header", "title": "Step title", "description": "Detailed instructions (supports [VAR:variable_name] interpolation)", "content_type": "action | informational | verification | warning", "estimated_minutes": 5, "commands": [{ "code": "Get-Service ...", "label": "Check service", "language": "powershell" }], "expected_outcome": "What success looks like", "verification_prompt": "Confirm the service is running", "verification_type": "checkbox | text_input", "warning_text": "Caution text for warning content_type", "notes_enabled": true, "reference_url": "https://docs.microsoft.com/...", "section_header": "Optional section label" } ``` ### Structural Rules - `steps` array must be non-empty - Each step needs `id`, `type`, `title` - Valid types: `procedure_step`, `procedure_end`, `section_header` - Exactly ONE `procedure_end` as the LAST step - No duplicate step IDs - `content_type` if present must be: `action`, `informational`, `verification`, `warning` - Commands can be a string or array of `{ code, label?, language? }` ### Intake Form (Optional) Procedural flows can have an intake form that captures variables before execution. Fields use `variable_name` (e.g., `server_name`) referenced in step descriptions as `[VAR:server_name]`. --- ## Task 1: Add Procedural System Prompts to AI Chat Service **Files:** - Modify: `backend/app/core/ai_chat_service.py` **Step 1: Add procedural schema context constant** After the existing `SCHEMA_CONTEXT` constant (~line 78), add: ```python PROCEDURAL_SCHEMA_CONTEXT = """ PROCEDURAL STEP SCHEMA — This is what you are building: Procedural flows are a FLAT ORDERED ARRAY of steps (NOT a branching tree). The structure is: {"steps": [step1, step2, ..., end_step]} Each step has a "type" field: 1. procedure_step — A task the engineer performs Required: id (string), type ("procedure_step"), title (string) Optional: description (string — detailed instructions, supports [VAR:variable_name] interpolation), content_type ("action" | "informational" | "verification" | "warning"), estimated_minutes (integer), commands (array of {code: string, label?: string, language?: string}), expected_outcome (string), verification_prompt (string — question to confirm step completion), verification_type ("checkbox" | "text_input"), warning_text (string — caution text, used with content_type "warning"), notes_enabled (boolean, default true), reference_url (string — documentation link) 2. section_header — A visual divider to organize steps into phases Required: id (string), type ("section_header"), title (string) Optional: description (string) 3. procedure_end — The final completion marker (exactly ONE, always LAST) Required: id (string), type ("procedure_end"), title (string) Optional: description (string — completion summary text) CONTENT TYPES for procedure_step: - "action" (default): Executable task with commands — shows terminal icon - "informational": Read-only context or reference info — shows info icon - "verification": Requires engineer confirmation before proceeding — shows checkmark icon - "warning": Highlighted caution/danger step — shows alert icon STRUCTURAL RULES: - Steps are executed in array order — position determines sequence - All IDs must be unique strings (use descriptive slugs like "install-ad-ds-role") - The LAST step MUST be type "procedure_end" - Section headers group related steps visually but don't affect execution order - Use [VAR:variable_name] in descriptions to reference intake form variables (e.g., "Configure IP on [VAR:server_name]") COMMAND FORMAT: Commands are arrays of objects, each with: - code (required): The exact command syntax (PowerShell, CMD, bash, etc.) - label (optional): Short description of what the command does - language (optional): "powershell", "cmd", "bash", etc. """ PROCEDURAL_INTERVIEW_PROTOCOL = """ INTERVIEW PHASES — Follow this progression: PHASE 1 - SCOPING (current_phase: scoping): Understand what procedure this flow covers: - What process is this flow for? (e.g., "new domain controller build", "Exchange migration", "firewall replacement") - What is the target environment? (on-prem, hybrid, cloud, specific vendors?) - Who will execute this? (Tier level, experience assumptions) - What information will the engineer need before starting? (This becomes the intake form — server name, IP, domain, credentials, etc.) Demonstrate expertise: "For a DC build, we'd typically need server name, IP, subnet, gateway, domain name, DSRM password, and whether this is the first DC or joining an existing domain." DO NOT emit [STEPS_UPDATE] during scoping. PHASE 2 - DISCOVERY (current_phase: discovery): Build out the procedure step by step: - Establish the major phases (these become section_headers) - For each phase, work through the steps in execution order - Capture specific commands with exact syntax - Add verification steps where the engineer should confirm something before proceeding - Add warning steps for anything destructive or irreversible EMIT [STEPS_UPDATE] when you and the user have agreed on concrete steps. Include ALL steps discussed so far. PHASE 3 - ENRICHMENT (current_phase: enrichment): Circle back to improve existing steps: - Add exact PowerShell/CLI commands with syntax - Add expected_outcome for action steps - Add verification prompts for critical checkpoints - Add estimated_minutes for time-sensitive procedures - Add reference_url links to relevant documentation - Add warning_text for dangerous operations - Suggest intake form variables for values that change per execution EMIT [STEPS_UPDATE] when enriching steps. PHASE 4 - REVIEW (current_phase: review): Present a summary: - Total step count by content_type - Section-by-section outline - Estimated total time - List of intake form variables suggested - Flag any gaps or areas needing more detail EMIT [STEPS_UPDATE] only if the user requests changes. TRANSITION between phases by emitting [PHASE:phase_name] when the conversation naturally moves to the next stage. """ PROCEDURAL_RESPONSE_FORMAT = """ RESPONSE FORMAT: Your response is natural conversational text. When the step structure changes, include structured markers that will be parsed by the system (the user will NOT see these markers): 1. Steps update (only when structure changes — see phase rules above): [STEPS_UPDATE] {"steps": [...valid steps array...]} [/STEPS_UPDATE] 2. Phase transition (when moving to next phase): [PHASE:discovery] 3. Metadata capture (when you learn the flow's name, description, or tags): [METADATA] {"name": "...", "description": "...", "tags": ["..."]} [/METADATA] 4. Intake form suggestion (when you identify variables the engineer will need): [INTAKE_FORM] [{"variable_name": "server_name", "label": "Server Name", "field_type": "text", "required": true, "placeholder": "e.g., DC01", "group_name": "Server Details", "display_order": 1}] [/INTAKE_FORM] IMPORTANT: - Include [STEPS_UPDATE] sparingly. Only when concrete steps are established or modified. - The steps update should be the COMPLETE working steps array, not a diff. - Always include conversational text OUTSIDE the markers — never respond with only markers. - The last step in the array MUST always be type "procedure_end". """ ``` **Step 2: Update `_build_system_prompt` to dispatch by flow_type** Replace the existing `_build_system_prompt` function: ```python def _build_system_prompt(flow_type: str) -> str: """Assemble the full system prompt for the chat builder.""" if flow_type in ("procedural", "maintenance"): flow_context = ( f"The user wants to build a {'MAINTENANCE' if flow_type == 'maintenance' else 'PROCEDURAL'} flow — " "a step-by-step process guide that walks engineers through a procedure in sequence. " "Steps are executed in order, not branching paths." ) return f"{ROLE_PERSONA}\n\n{flow_context}\n\n{PROCEDURAL_SCHEMA_CONTEXT}\n\n{PROCEDURAL_INTERVIEW_PROTOCOL}\n\n{PROCEDURAL_RESPONSE_FORMAT}" else: flow_context = ( "The user wants to build a TROUBLESHOOTING flow — a diagnostic decision tree " "that guides engineers through symptom identification, diagnostic checks, and " "resolution steps." ) return f"{ROLE_PERSONA}\n\n{flow_context}\n\n{SCHEMA_CONTEXT}\n\n{INTERVIEW_PROTOCOL}\n\n{RESPONSE_FORMAT}" ``` **Step 3: Update `_parse_ai_response` to handle `[STEPS_UPDATE]` and `[INTAKE_FORM]`** Add extraction for the new markers. After the `[METADATA]` extraction block: ```python # Extract [STEPS_UPDATE]...[/STEPS_UPDATE] steps_match = re.search( r"\[STEPS_UPDATE\]\s*([\s\S]*?)\s*\[/STEPS_UPDATE\]", result["content"] ) if steps_match: try: raw_json = _strip_markdown_fences(steps_match.group(1)) result["tree_update"] = json.loads(raw_json) except (json.JSONDecodeError, ValueError) as e: logger.warning("Failed to parse steps update JSON: %s", e) result["content"] = result["content"][: steps_match.start()] + result["content"][steps_match.end() :] else: truncated_steps = re.search(r"\[STEPS_UPDATE\][\s\S]*$", result["content"]) if truncated_steps: logger.warning("Truncated [STEPS_UPDATE] block detected — stripping from display") result["content"] = result["content"][: truncated_steps.start()] # Extract [INTAKE_FORM]...[/INTAKE_FORM] intake_match = re.search( r"\[INTAKE_FORM\]\s*([\s\S]*?)\s*\[/INTAKE_FORM\]", result["content"] ) if intake_match: try: raw_json = _strip_markdown_fences(intake_match.group(1)) result["intake_form"] = json.loads(raw_json) except (json.JSONDecodeError, ValueError) as e: logger.warning("Failed to parse intake form JSON: %s", e) result["content"] = result["content"][: intake_match.start()] + result["content"][intake_match.end() :] else: truncated_intake = re.search(r"\[INTAKE_FORM\][\s\S]*$", result["content"]) if truncated_intake: logger.warning("Truncated [INTAKE_FORM] block detected — stripping from display") result["content"] = result["content"][: truncated_intake.start()] ``` Also add `"intake_form": None` to the initial `result` dict. **Step 4: Update `send_message` to validate procedural structure** In `send_message()`, replace the tree_update validation block (~line 320-326): ```python # Validate tree update if present tree_update = parsed["tree_update"] if tree_update: if session.flow_type in ("procedural", "maintenance"): # Procedural: must have a steps array if not isinstance(tree_update, dict) or not isinstance(tree_update.get("steps"), list): logger.warning("AI steps update rejected: must have a steps array") tree_update = None else: # Troubleshooting: root must be a decision node if not isinstance(tree_update, dict) or tree_update.get("type") != "decision": logger.warning("AI tree update rejected: root must be a decision node") tree_update = None elif not tree_update.get("id"): logger.warning("AI tree update rejected: root node missing id") tree_update = None ``` Also handle intake_form persistence after the metadata block: ```python if parsed.get("intake_form"): session.intake_form_draft = parsed["intake_form"] ``` Wait — `AIChatSession` may not have an `intake_form_draft` field. We'll store it in `tree_metadata` instead: ```python if parsed.get("intake_form"): merged = dict(session.tree_metadata) if session.tree_metadata else {} merged["intake_form"] = parsed["intake_form"] session.tree_metadata = merged ``` **Step 5: Update `generate_final_tree` for procedural flows** Replace the `generation_instruction` string with flow-type-aware instructions: ```python if session.flow_type in ("procedural", "maintenance"): generation_instruction = """Based on our entire conversation, generate the COMPLETE and FINAL procedural steps JSON for this flow. Requirements: - Include ALL steps we discussed, organized into sections - Use descriptive step IDs (slugs, not UUIDs) - Each step needs: id, type, title, description - Include commands with exact syntax where discussed - Include content_type for each step (action, informational, verification, warning) - Include estimated_minutes where discussed - Include verification_prompt for verification steps - Include warning_text for warning steps - The LAST step MUST be type "procedure_end" - Respond with ONLY the JSON — no conversational text, no markdown fences Format: {"steps": [step1, step2, ..., end_step]} Also provide metadata as a separate JSON object after the steps: [METADATA] {"name": "...", "description": "...", "tags": ["..."]} [/METADATA] If we discussed intake form variables, include them: [INTAKE_FORM] [{"variable_name": "...", "label": "...", "field_type": "text", "required": true, "display_order": 1}] [/INTAKE_FORM]""" else: generation_instruction = """Based on our entire conversation, generate the COMPLETE and FINAL TreeStructure JSON for this flow. ...existing troubleshooting instruction...""" ``` Update the validation inside the generation loop to handle procedural: ```python if not tree: # ... existing retry logic ... if session.flow_type in ("procedural", "maintenance"): # Validate procedural structure p_errors = validate_generated_procedural_steps(tree) if p_errors: if attempt == 0: # ... retry with correction ... continue raise ValueError(f"Generated steps failed validation: {'; '.join(p_errors)}") else: errors = validate_generated_tree(tree) if errors: # ... existing retry logic ... ``` **Step 6: Run backend tests** Run: `cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/test_ai_chat.py -v --override-ini="addopts="` Expected: All existing tests pass (they test troubleshooting flow). **Step 7: Commit** ```bash git add backend/app/core/ai_chat_service.py git commit -m "feat: add procedural flow prompts to AI chat builder" ``` --- ## Task 2: Add Procedural Validation to AI Tree Validator **Files:** - Modify: `backend/app/core/ai_tree_validator.py` **Step 1: Add `validate_generated_procedural_steps` function** Add after the existing `count_tree_stats` function: ```python VALID_PROCEDURAL_STEP_TYPES = {"procedure_step", "procedure_end", "section_header"} VALID_CONTENT_TYPES = {"action", "informational", "verification", "warning"} def validate_generated_procedural_steps(tree: dict[str, Any]) -> list[str]: """Validate an AI-generated procedural steps structure. Returns a list of error strings. Empty list means valid. """ errors: list[str] = [] if not isinstance(tree, dict): return ["Steps structure must be a JSON object"] steps = tree.get("steps") if not isinstance(steps, list) or len(steps) == 0: return ["Must have a non-empty 'steps' array"] seen_ids: set[str] = set() end_count = 0 step_count = 0 for i, step in enumerate(steps): if not isinstance(step, dict): errors.append(f"Step at index {i} is not an object") continue step_id = step.get("id") step_type = step.get("type") # Check ID if not step_id: errors.append(f"Step at index {i} missing 'id'") elif step_id in seen_ids: errors.append(f"Duplicate step ID: '{step_id}'") else: seen_ids.add(step_id) # Check type if step_type not in VALID_PROCEDURAL_STEP_TYPES: errors.append( f"Step '{step_id or i}' has invalid type '{step_type}'. " f"Must be one of: {', '.join(sorted(VALID_PROCEDURAL_STEP_TYPES))}" ) continue # Check title if not step.get("title"): errors.append(f"Step '{step_id}' missing 'title'") # Content type validation content_type = step.get("content_type") if content_type and content_type not in VALID_CONTENT_TYPES: errors.append( f"Step '{step_id}' has invalid content_type '{content_type}'. " f"Must be one of: {', '.join(sorted(VALID_CONTENT_TYPES))}" ) if step_type == "procedure_step": step_count += 1 elif step_type == "procedure_end": end_count += 1 # Structural checks if end_count == 0: errors.append("Must have exactly one 'procedure_end' step as the last step") elif end_count > 1: errors.append(f"Found {end_count} procedure_end steps, must have exactly 1") if end_count == 1 and steps[-1].get("type") != "procedure_end": errors.append("The procedure_end step must be the last step in the array") if step_count < 2: errors.append( f"Flow has only {step_count} procedure steps. " "Need at least 2 for a useful procedure." ) if len(steps) > 100: errors.append(f"Flow has {len(steps)} steps. Maximum 100 allowed.") return errors ``` **Step 2: Run tests** Run: `cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/ -k "procedural" -v --override-ini="addopts="` Expected: Existing procedural tests pass. **Step 3: Commit** ```bash git add backend/app/core/ai_tree_validator.py git commit -m "feat: add procedural steps validator for AI-generated flows" ``` --- ## Task 3: Handle Intake Form + Procedural Import in AI Chat Endpoint **Files:** - Modify: `backend/app/api/endpoints/ai_chat.py` **Step 1: Update the `import_tree` endpoint to handle intake form from metadata** In the `import_tree` function (~line 393), after building the Tree object, check for intake form: ```python # Extract intake form from metadata if present intake_form = None if metadata.get("intake_form"): intake_form = metadata.pop("intake_form") tree = Tree( name=data.name or metadata.get("name", "AI-Generated Flow"), description=data.description or metadata.get("description", ""), tree_type=session.flow_type, tree_structure=session.working_tree, intake_form=intake_form, author_id=current_user.id, account_id=current_user.account_id, category_id=data.category_id, is_public=False, ) ``` **Step 2: Run tests** Run: `cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/test_ai_chat.py -v --override-ini="addopts="` Expected: All tests pass. **Step 3: Commit** ```bash git add backend/app/api/endpoints/ai_chat.py git commit -m "feat: handle intake form in AI chat procedural import" ``` --- ## Task 4: Add Procedural Steps Preview Component (Frontend) **Files:** - Create: `frontend/src/components/ai-chat/StaticStepsPreview.tsx` **Step 1: Create the procedural steps preview component** ```tsx import type { ProceduralStep } from '@/types' import { Terminal, Info, CheckSquare, AlertTriangle, LayoutList } from 'lucide-react' import { cn } from '@/lib/utils' interface StaticStepsPreviewProps { steps: ProceduralStep[] name?: string } const CONTENT_TYPE_ICONS: Record = { action: Terminal, informational: Info, verification: CheckSquare, warning: AlertTriangle, } export function StaticStepsPreview({ steps, name }: StaticStepsPreviewProps) { let stepNumber = 0 return (

Preview: {name || 'Untitled Flow'}

{steps.filter((s) => s.type === 'procedure_step').length} steps

{steps.map((step) => { if (step.type === 'section_header') { return (
{step.title}
) } if (step.type === 'procedure_end') { return (
{step.title || 'Procedure Complete'}
) } stepNumber++ const contentType = step.content_type || 'action' const Icon = CONTENT_TYPE_ICONS[contentType] || Terminal return (
{stepNumber}
{step.title}
{step.commands && (
{Array.isArray(step.commands) ? step.commands.length : 1} command{(Array.isArray(step.commands) ? step.commands.length : 1) !== 1 ? 's' : ''}
)}
{step.estimated_minutes && ( ~{step.estimated_minutes}m )}
) })}
) } ``` **Step 2: Run build** Run: `cd /home/michaelchihlas/dev/patherly/frontend && npm run build` Expected: Build passes. **Step 3: Commit** ```bash git add frontend/src/components/ai-chat/StaticStepsPreview.tsx git commit -m "feat: add procedural steps preview component for AI chat builder" ``` --- ## Task 5: Update AI Chat Store + Page for Procedural Flows **Files:** - Modify: `frontend/src/store/aiChatStore.ts` - Modify: `frontend/src/pages/AIChatBuilderPage.tsx` **Step 1: Update `AIChatState` interface and `sendMessage` handler in store** In `aiChatStore.ts`, update the `workingTree` type to also accept procedural structure: ```typescript // Change line 29: workingTree: TreeStructure | { steps: ProceduralStep[] } | null // Change line 33: generatedTree: TreeStructure | { steps: ProceduralStep[] } | null ``` Add `ProceduralStep` to the imports: ```typescript import type { ChatMessage, InterviewPhase, TreeStructure, ProceduralStep, } from '@/types' ``` Update `sendMessage` (~line 121-127) — the response handling already works because `working_tree` is stored as-is from the API. The cast just needs updating: ```typescript workingTree: (response.working_tree as TreeStructure | { steps: ProceduralStep[] } | null) ?? state.workingTree, ``` And in `generateTree` (~line 142-143): ```typescript generatedTree: response.tree_structure as unknown as TreeStructure | { steps: ProceduralStep[] }, workingTree: response.tree_structure as unknown as TreeStructure | { steps: ProceduralStep[] }, ``` And in `resumeSession` (~line 185-187): ```typescript workingTree: session.working_tree as TreeStructure | { steps: ProceduralStep[] } | null, generatedTree: session.generated_tree as TreeStructure | { steps: ProceduralStep[] } | null, ``` **Step 2: Update `AIChatBuilderPage.tsx` to render correct preview** Add import for `StaticStepsPreview` and `ProceduralStep`: ```typescript import { StaticStepsPreview } from '@/components/ai-chat/StaticStepsPreview' import type { ProceduralStep } from '@/types' ``` Replace the preview tree logic (~line 116) and preview render (~line 143-151): ```typescript const previewData = generatedTree || workingTree // Determine if this is a procedural preview const isProceduralPreview = previewData && 'steps' in previewData // ... in the JSX: {previewData ? ( isProceduralPreview ? ( ) : ( ) ) : ( )} ``` Remove the now-unused `const previewTree = (generatedTree || workingTree) as TreeStructure | null` line. **Step 3: Run build** Run: `cd /home/michaelchihlas/dev/patherly/frontend && npm run build` Expected: Build passes. **Step 4: Commit** ```bash git add frontend/src/store/aiChatStore.ts frontend/src/pages/AIChatBuilderPage.tsx git commit -m "feat: wire procedural steps preview into AI chat builder page" ``` --- ## Task 6: Update `generate_final_tree` Generation + Validation Wiring **Files:** - Modify: `backend/app/core/ai_chat_service.py` This task ensures the full `generate_final_tree` function properly handles the procedural path end-to-end, including the retry loop and validation import. **Step 1: Add import for the new validator** ```python from app.core.ai_tree_validator import validate_generated_tree, validate_generated_procedural_steps ``` **Step 2: Update the validation block in `generate_final_tree`** Inside the `for attempt in range(2)` loop, after tree is extracted, replace the validation block: ```python if session.flow_type in ("procedural", "maintenance"): val_errors = validate_generated_procedural_steps(tree) else: val_errors = validate_generated_tree(tree) if val_errors: if attempt == 0: provider_messages.append({"role": "assistant", "content": response_text}) correction = ( f"The generated structure has validation errors: {'; '.join(val_errors)}. " "Please fix these issues and respond with the corrected JSON only." ) provider_messages.append({"role": "user", "content": correction}) continue raise ValueError(f"Generated structure failed validation: {'; '.join(val_errors)}") ``` **Step 3: Handle intake form from final generation** After the `# Success` comment, before returning: ```python # Extract intake form from metadata if present if parsed.get("intake_form") and isinstance(parsed["intake_form"], list): metadata["intake_form"] = parsed["intake_form"] ``` **Step 4: Run backend tests** Run: `cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/test_ai_chat.py -v --override-ini="addopts="` Expected: All tests pass. **Step 5: Commit** ```bash git add backend/app/core/ai_chat_service.py git commit -m "feat: wire procedural validation into AI chat generate flow" ``` --- ## Task 7: Final Integration Test + Build Verification **Step 1: Run full backend test suite** Run: `cd /home/michaelchihlas/dev/patherly/backend && python -m pytest tests/ --override-ini="addopts=" -v` Expected: All tests pass. **Step 2: Run frontend build** Run: `cd /home/michaelchihlas/dev/patherly/frontend && npm run build` Expected: Build passes with zero errors. **Step 3: Final commit (if any remaining changes)** ```bash git add -A git commit -m "chore: final cleanup for procedural Flow Assist support" ``` --- ## Summary of Changes | File | Change | |------|--------| | `backend/app/core/ai_chat_service.py` | Add procedural schema/protocol/format prompts, dispatch by flow_type, parse `[STEPS_UPDATE]` + `[INTAKE_FORM]`, validate procedural structure, procedural generation instruction | | `backend/app/core/ai_tree_validator.py` | Add `validate_generated_procedural_steps()` function | | `backend/app/api/endpoints/ai_chat.py` | Handle intake form in import endpoint | | `frontend/src/components/ai-chat/StaticStepsPreview.tsx` | New procedural steps preview component | | `frontend/src/store/aiChatStore.ts` | Widen `workingTree`/`generatedTree` types for procedural | | `frontend/src/pages/AIChatBuilderPage.tsx` | Render `StaticStepsPreview` for procedural flows |