6.4 KiB
Procedural Custom Steps — Design
Date: February 24, 2026 Status: Approved Phase: 2.5 — Feature 2 of 3
What We're Building
Add "Add Custom Step" support to ProceduralNavigationPage so engineers can insert ad-hoc steps between any existing checklist items during execution. The inserted step appears inline in the checklist and detail panel — worked through just like a regular step before proceeding.
Key Difference from Troubleshooting Custom Steps
useCustomStepFlow is built for tree-graph navigation: currentNodeId, findNode, setCurrentNodeId, path traversal, continuation modal for picking descendants, custom branch mode. None of that applies here.
Procedural flows are linear arrays. A custom step is just a new ProceduralStep-shaped object injected at a position in the array. No new hook needed — all state lives in ProceduralNavigationPage.
Data Model
Custom steps are stored in session custom_steps (already a JSONB array on Session). The existing CustomStep type is:
interface CustomStep {
id: string
inserted_after_node_id: string // step ID it was inserted after
step_data: Step | CustomStepDraft
timestamp: string
}
For procedural flows, inserted_after_node_id is the ProceduralStep.id of the step it follows.
A custom step is represented in the runtime ProceduralStep[] as:
{
id: customStep.id, // the CustomStep UUID
type: 'procedure_step',
title: step_data.title,
description: step_data.content.instructions,
content_type: 'action',
commands: step_data.content.commands (mapped),
// marker for custom steps:
_isCustom: true // not in ProceduralStep type — use a local union
}
Rather than mutating ProceduralStep, we use a local discriminated union:
type RuntimeStep = ProceduralStep | CustomProceduralStep
interface CustomProceduralStep {
id: string
type: 'procedure_step'
title: string
description?: string
content_type: 'action'
commands?: CommandBlock[]
isCustom: true // discriminant
}
The procedureSteps array used for rendering becomes RuntimeStep[] instead of ProceduralStep[].
User Flow
- Engineer is on any step in a procedural flow
- Clicks "+ Add Step" button below the current step detail
CustomStepModalopens (existing component — Create tab + Browse Library tab)- Engineer creates or selects a step →
PostStepActionModalappears - "Use Now": inserts after current step, closes modals, advances to the new step
- "Save for Later": saves to library only, no insertion
- "Do Both": saves to library + inserts
The inserted step renders in StepDetail and StepChecklist with a visual "Custom" badge. The engineer marks it complete the same way as any other step (the "Mark Complete" button).
No continuation modal. No custom branch mode. No fork flow.
What Changes
ProceduralNavigationPage.tsx
New state:
const [runtimeSteps, setRuntimeSteps] = useState<RuntimeStep[]>([])
const [showCustomStepModal, setShowCustomStepModal] = useState(false)
const [showPostStepModal, setShowPostStepModal] = useState(false)
const [pendingCustomStep, setPendingCustomStep] = useState<Step | CustomStepDraft | null>(null)
const [pendingIsFromLibrary, setPendingIsFromLibrary] = useState(false)
const [isSavingStep, setIsSavingStep] = useState(false)
const [sessionCustomSteps, setSessionCustomSteps] = useState<CustomStep[]>([])
runtimeSteps is initialized from tree.tree_structure.steps and updated when custom steps are inserted. procedureSteps (currently steps.filter(s => s.type === 'procedure_step')) becomes runtimeSteps.filter(...).
handleInsertCustomStep(step, isFromLibrary):
- Builds a
CustomProceduralStepfrom the draft/step - Inserts it into
runtimeStepsaftercurrentStepIndex - Adds a
CustomStepentry tosessionCustomSteps - Calls
sessionsApi.update(session.id, { custom_steps: newCustomSteps }) - Advances
currentStepIndexby 1 (focus moves to the new step)
handleStepCreated(step, isFromLibrary): — same pattern as troubleshooting
- Sets
pendingCustomStep,pendingIsFromLibrary - Closes
CustomStepModal, opensPostStepActionModal
Add handleSaveForLater, handleUseNow, handleBoth — same logic as useCustomStepFlow but without path/node navigation.
"+ Add Step" button: Renders in the right panel below StepDetail, above StepFeedback. Only shown on the current active (incomplete) step. Not shown on already-completed steps or on custom steps (no nesting).
Session resume: On resumeSession, initialize runtimeSteps from the tree steps then inject custom steps from sessionData.custom_steps at the correct positions.
StepChecklist.tsx
Accept RuntimeStep[] instead of ProceduralStep[]. Render a small "Custom" badge (amber dot or label) next to custom step titles.
StepDetail.tsx
Accept RuntimeStep instead of ProceduralStep. When step.isCustom === true, render slightly differently: no content_type badge (use a plain "Custom Step" label instead), show instructions as description, render commands if present using the existing command block renderer.
New type: RuntimeStep
Add to frontend/src/types/tree.ts (or a separate procedural.ts):
export interface CustomProceduralStep {
id: string
type: 'procedure_step'
title: string
description?: string
content_type: 'action'
commands?: CommandBlock[]
isCustom: true
}
export type RuntimeStep = ProceduralStep | CustomProceduralStep
What Does NOT Change
useCustomStepFlow— not used at all in procedural flowsContinuationModal— not used (no branching)- Fork flow — not used
CustomStepModal— reused as-is (already works for both create and browse)PostStepActionModal— reused as-issessionsApi.updatewithcustom_steps— already supports this- Backend — no changes needed
Checklist render (sidebar)
Custom steps show in the checklist with an amber ✦ or small "Custom" chip:
✅ 1. Check service status
✅ 2. Restart broker agent
3. [✦ Custom] Verify VDA re-registration ← custom step
4. Check event log
Completion behavior
When the engineer marks the last custom step complete and it was inserted before the last regular step, currentStepIndex increments normally — they continue with the remaining regular steps. The total step count in the progress bar updates when custom steps are inserted.