Planned Feature - This component is not yet implemented.
See GitHub issues for design details and implementation plan.
Switch Component
Multi-way branching based on runtime value evaluation with persisted decision and gated execution. Enables state machine transitions, mode selection, and complex branching logic while maintaining plan visibility.
Planned API
interface SwitchProps<T = string> {
/**
* Stable identifier for resumability. Required.
*/
id: string
/**
* Value to match against cases.
* Can be static value or function for dynamic evaluation.
*/
value: T | (() => T | Promise<T>)
/**
* Optional serializer for non-primitive values.
* Required for objects/arrays to persist correctly.
*/
serialize?: (value: T) => string
/**
* Optional deserializer for persisted values.
* Required if serialize provided.
*/
deserialize?: (raw: string) => T
/**
* Children should be <Case> and optionally <Default> components.
*/
children: ReactNode
}
interface CaseProps<T = string> {
/**
* Value(s) to match. Single value or array for multiple matches.
*/
match: T | T[]
/**
* Children to render when matched.
*/
children: ReactNode
}
interface DefaultProps {
/**
* Children to render when no case matches.
*/
children: ReactNode
}
export function Switch<T = string>(props: SwitchProps<T>): JSX.Element
export function Case<T = string>(props: CaseProps<T>): JSX.Element
export function Default(props: DefaultProps): JSX.Element
Proposed Usage
Basic Mode Selection
import { Switch, Case, Default, Phase, Step, Claude, useSmithers } from 'smithers-orchestrator'
export function WorkflowModeSwitch() {
const { db } = useSmithers()
return (
<Switch
id="workflow-mode"
value={async () => await db.state.get('workflowMode')}
>
<Case match="fast">
<Phase name="Quick Implementation">
<Step name="quick-implement">
<Claude model="haiku">Implement quickly with minimal checks</Claude>
</Step>
</Phase>
</Case>
<Case match="thorough">
<Phase name="Research">
<Step name="research">
<Claude model="opus">Deep research and planning</Claude>
</Step>
</Phase>
<Phase name="Implementation">
<Step name="implement">
<Claude model="sonnet">Careful implementation with tests</Claude>
</Step>
</Phase>
</Case>
<Case match="experimental">
<Phase name="Prototype">
<Step name="prototype">
<Claude>Build experimental prototype</Claude>
</Step>
</Phase>
</Case>
<Default>
<Phase name="Standard Flow">
<Step name="standard">
<Claude>Standard implementation process</Claude>
</Step>
</Phase>
</Default>
</Switch>
)
}
Multiple Match Values
<Switch
id="priority-routing"
value={async () => {
const issue = await db.state.get('currentIssue')
return issue.priority
}}
>
<Case match={["critical", "high"]}>
<Phase name="Urgent Response">
<Step name="urgent">
<Claude model="opus">Immediate attention with senior review</Claude>
</Step>
</Phase>
</Case>
<Case match="medium">
<Phase name="Standard Response">
<Step name="standard">
<Claude model="sonnet">Standard workflow</Claude>
</Step>
</Phase>
</Case>
<Case match={["low", "trivial"]}>
<Phase name="Batch Processing">
<Step name="batch">
<Claude model="haiku">Queue for batch processing</Claude>
</Step>
</Phase>
</Case>
<Default>
<Phase name="Triage">
<Step name="triage">
<Claude>Assess and categorize issue</Claude>
</Step>
</Phase>
</Default>
</Switch>
State Machine Transitions
<Switch
id="deployment-state"
value={async () => {
const { db } = useSmithers()
const state = await db.state.get('deploymentState')
return state || 'init'
}}
>
<Case match="init">
<Phase name="Initialize">
<Step name="init">
<Claude>Set up deployment environment</Claude>
</Step>
</Phase>
</Case>
<Case match="building">
<Phase name="Build">
<Step name="build">
<Claude>Build application artifacts</Claude>
</Step>
</Phase>
</Case>
<Case match="testing">
<Phase name="Test">
<Step name="test">
<Claude>Run integration tests</Claude>
</Step>
</Phase>
</Case>
<Case match="deploying">
<Phase name="Deploy">
<Step name="deploy">
<Claude>Deploy to production</Claude>
</Step>
</Phase>
</Case>
<Case match="complete">
<Phase name="Verify">
<Step name="verify">
<Claude>Verify deployment health</Claude>
</Step>
</Phase>
</Case>
<Default>
<Phase name="Error Recovery">
<Step name="recover">
<Claude>Handle unknown state</Claude>
</Step>
</Phase>
</Default>
</Switch>
Non-Primitive Values with Serializer
interface TaskType {
category: string
complexity: number
}
<Switch
id="task-routing"
value={async () => {
const task = await db.state.get('currentTask')
return task as TaskType
}}
serialize={(t) => JSON.stringify(t)}
deserialize={(s) => JSON.parse(s) as TaskType}
>
<Case match={{ category: 'bug', complexity: 1 }}>
<Phase name="Quick Fix">
<Step name="quick-fix">
<Claude model="haiku">Apply simple bugfix</Claude>
</Step>
</Phase>
</Case>
<Case match={{ category: 'feature', complexity: 3 }}>
<Phase name="Complex Feature">
<Step name="complex-feature">
<Claude model="opus">Plan and implement complex feature</Claude>
</Step>
</Phase>
</Case>
<Default>
<Phase name="Standard Task">
<Step name="standard-task">
<Claude model="sonnet">Handle standard task</Claude>
</Step>
</Phase>
</Default>
</Switch>
Props (Planned)
Stable identifier for component identity across restarts.Used to build case scope IDs: {scopeId}.switch.{id}.case{N} or {scopeId}.switch.{id}.defaultExamples:
"workflow-mode" - Describes what’s being switched on
"priority-routing" - Indicates routing logic
- ❌ Random ID - Breaks resumability
value
T | (() => T | Promise<T>)
required
Value to match against case conditions.Static value:<Switch id="mode" value="production">
Sync function:<Switch id="mode" value={() => computeMode()}>
Async function (database query):<Switch
id="mode"
value={async () => {
const { db } = useSmithers()
return await db.state.get('currentMode')
}}
>
Persistence: Evaluated once per scope, result stored in SQLite for crash-resume.Task gating: Holds control-flow task during evaluation.
Serializer for non-primitive values.Required when value is object/array to enable correct persistence and comparison.serialize={(obj) => JSON.stringify(obj)}
Default: (value) => String(value) (works for primitives)
Deserializer for persisted values.Required if serialize provided. Must be inverse of serialize.deserialize={(str) => JSON.parse(str) as MyType}
Default: (raw) => raw as unknown as T
Should contain <Case> and optionally <Default> components.Each case executed under isolated ExecutionBoundary:
- Case 0:
root.switch.mode.case0
- Case 1:
root.switch.mode.case1
- Default:
root.switch.mode.default
Execution: Only matched case/default has executionEnabled=true. Others render in plan-only mode.
Case Component Props
Value(s) to match against switch value.Single value:<Case match="production">
Multiple values (OR logic):<Case match={["staging", "development"]}>
Matching: Uses strict equality (===) for primitives. For objects, uses serialized comparison.
Components rendered when case matches.
Default Component Props
Components rendered when no case matches.Optional - if omitted and no case matches, Switch completes without executing children.
Implementation Status
Prerequisites (Pending)
Requires ExecutionBoundary, makeScopeId, makeStateKey, tasks.scope_id.
Implementation (Pending)
Component implementation, case matching logic, serialization support.
Testing (Future)
Unit tests for primitive/object matching, default fallthrough, nested switches.
Design Rationale
Why Not If Chains?
// With If chains (verbose, error-prone)
<If id="check-fast" condition={() => mode === 'fast'}>
<Phase name="Fast">
<Step name="fast">...</Step>
</Phase>
</If>
<If id="check-thorough" condition={() => mode === 'thorough'}>
<Phase name="Thorough">
<Step name="thorough">...</Step>
</Phase>
<Else>
<If id="check-experimental" condition={() => mode === 'experimental'}>
<Phase name="Experimental">
<Step name="experimental">...</Step>
</Phase>
<Else>
<Phase name="Default">
<Step name="default">...</Step>
</Phase>
</Else>
</If>
</Else>
</If>
// With Switch (clean, intent clear)
<Switch id="mode" value={mode}>
<Case match="fast">
<Phase name="Fast">
<Step name="fast">...</Step>
</Phase>
</Case>
<Case match="thorough">
<Phase name="Thorough">
<Step name="thorough">...</Step>
</Phase>
</Case>
<Case match="experimental">
<Phase name="Experimental">
<Step name="experimental">...</Step>
</Phase>
</Case>
<Default>
<Phase name="Default">
<Step name="default">...</Step>
</Phase>
</Default>
</Switch>
Plan Output
<switch id="workflow-mode" value="thorough" status="evaluated">
<case match="fast" active="false">
<phase name="Quick Implementation" status="skipped">...</phase>
</case>
<case match="thorough" active="true">
<phase name="Research" status="completed">...</phase>
<phase name="Implementation" status="active">...</phase>
</case>
<case match="experimental" active="false">
<phase name="Prototype" status="skipped">...</phase>
</case>
<default active="false">
<phase name="Standard Flow" status="skipped">...</phase>
</default>
</switch>
Shows all branches with clear indication of which matched.
Serialization for Complex Values
Objects/arrays need serialization for:
- Persistence - Store value in SQLite TEXT column
- Comparison - Match against case values after crash-resume
const taskTypeSerializer = {
serialize: (t: TaskType) => `${t.category}:${t.complexity}`,
deserialize: (s: string) => {
const [category, complexity] = s.split(':')
return { category, complexity: parseInt(complexity) }
}
}
Custom serializers enable efficient storage and comparison.
Examples of Use Cases
Use Case 1: Environment-Based Configuration
<Switch
id="environment"
value={process.env.NODE_ENV || 'development'}
>
<Case match="production">
<Phase name="Production Deploy">
<Step name="deploy">
<Claude>Deploy with production configuration and monitoring</Claude>
</Step>
</Phase>
</Case>
<Case match={["development", "test"]}>
<Phase name="Dev Deploy">
<Step name="deploy">
<Claude>Deploy with debug logging and test fixtures</Claude>
</Step>
</Phase>
</Case>
<Case match="staging">
<Phase name="Staging Deploy">
<Step name="deploy">
<Claude>Deploy to staging with production-like config</Claude>
</Step>
</Phase>
</Case>
<Default>
<Phase name="Unknown Environment">
<Step name="handle">
<Claude>Handle unknown environment - fail safe</Claude>
</Step>
</Phase>
</Default>
</Switch>
Use Case 2: Error Code Handling
<Switch
id="error-handler"
value={async () => {
const error = await db.state.get('lastError')
return error?.code
}}
>
<Case match="ECONNREFUSED">
<Phase name="Retry Connection">
<Step name="retry">
<Claude>Service unavailable - implement retry with backoff</Claude>
</Step>
</Phase>
</Case>
<Case match={["EACCES", "EPERM"]}>
<Phase name="Permission Error">
<Step name="permissions">
<Claude>Permission denied - check credentials and permissions</Claude>
</Step>
</Phase>
</Case>
<Case match="ENOENT">
<Phase name="Not Found">
<Step name="not-found">
<Claude>Resource not found - verify paths and create if needed</Claude>
</Step>
</Phase>
</Case>
<Default>
<Phase name="Generic Error Handler">
<Step name="handle">
<Claude>Handle unexpected error with logging and alerting</Claude>
</Step>
</Phase>
</Default>
</Switch>
Use Case 3: Nested Switches
<Switch id="platform" value={process.platform}>
<Case match="darwin">
<Switch id="arch" value={process.arch}>
<Case match="arm64">
<Phase name="macOS ARM">
<Step name="build">
<Claude>Build for Apple Silicon</Claude>
</Step>
</Phase>
</Case>
<Case match="x64">
<Phase name="macOS Intel">
<Step name="build">
<Claude>Build for Intel Mac</Claude>
</Step>
</Phase>
</Case>
</Switch>
</Case>
<Case match="linux">
<Phase name="Linux Build">
<Step name="build">
<Claude>Build for Linux</Claude>
</Step>
</Phase>
</Case>
<Case match="win32">
<Phase name="Windows Build">
<Step name="build">
<Claude>Build for Windows</Claude>
</Step>
</Phase>
</Case>
</Switch>
Alternatives Considered
- If/Else chains: Verbose, deeply nested, hard to read
- External routing logic: Hides flow from plan, not resumable
- Dynamic Phase names: Doesn’t work with Phase registry
- React conditional rendering: Hides structure from plan output
Feedback
If you have feedback on this planned component, please open an issue.