Router Node — Design Spec

Overview

A single Router node with two modes (Radio / Checkbox) that controls which downstream branches of a workflow execute. Supports configurable N routes with dynamic output handles.

Node Data Type

interface RouterNodeData {
  label: string;
  mode: "radio" | "checkbox";
  routes: Array<{
    id: string;        // crypto.randomUUID() — stable, never reused
    name: string;      // display name (e.g., "Route A")
    active: boolean;   // current activation state
  }>;
  // Execution result fields (set after execution)
  activeRoutes?: string[];          // IDs of active routes at execution time
  routeOutputs?: Record<string, any>; // routeId → passthrough value (or undefined)
  // Standard node fields
  status?: string;
  result?: string;
  progress?: number;
  skip?: boolean;
}

Default: 2 routes (“Route A”, “Route B”), first route active, Radio mode.

Route IDs use crypto.randomUUID() for stability — deleting and re-adding a route never reuses an old ID, so existing edges won’t silently reconnect to a different route.

Canvas Appearance

Node Body

Handles

Category

processing — same category as combine-text, split-text, and other control flow nodes.

Toolbar Group

New group in add-node-popup and node-toolbar: “Control Flow” (under Processing category).

Data Flow — Auto-Detect Passthrough

No explicit passthrough/gate toggle. Behavior is determined by connectivity:

This follows the QA Check node’s output routing pattern (sourceHandle-based conditional output), but uses combine-text/split-text’s execution model (inline, 0 credits). When a route returns undefined, downstream nodes connected to that route will still execute but receive empty input — this is consistent with QA Check behavior on the rejected branch.

Passthrough Type Detection

The Router is media-type agnostic. When passing data through, it forwards whatever the upstream node produced (URL string for images/video/audio, text string for prompts, etc.). The downstream input-resolver determines how to map the Router’s output based on URL pattern detection:

This uses the same URL-regex pattern as the teleporter/preview nodes.

Config Panel

Located in the processing configs section. Contains:

  1. Mode selector: Dropdown or segmented control — “Radio” / “Checkbox”
  2. Route list:
    • Each route: editable name field + delete button
    • Add route button at bottom
    • Minimum 2 routes enforced
    • Hard maximum 10 routes (prevents layout issues with too many handles)
  3. Active routes: Toggle/radio controls matching the canvas (so config panel and canvas stay in sync)

When switching from Checkbox to Radio mode: if multiple routes are active, keep only the first active one.

Execution Behavior

Frontend DAG Executor

execute-node.ts: Router executes inline (no API call, no credits):

// Pseudocode
const inputValue = resolveInput(node); // upstream value or undefined
const activeRoutes = node.data.routes.filter(r => r.active).map(r => r.id);
const routeOutputs: Record<string, any> = {};
for (const route of node.data.routes) {
  routeOutputs[route.id] = route.active ? (inputValue ?? "gate") : undefined;
}
updateNodeData(node.id, { activeRoutes, routeOutputs, result: "routed" });

extractNodeOutput() in execution-graph.ts: Reads from node.data fields:

case "router": {
  if (!sourceHandle) return node.data.result;
  // sourceHandle is the route UUID — return that route's output
  return node.data.routeOutputs?.[sourceHandle];
}

node-input-resolver.ts: Router as input source — detect media type from URL pattern:

case "router": {
  const value = extractNodeOutput(sourceNode, sourceHandle);
  if (!value || value === "gate") return { prompt: "" }; // gate mode
  if (isVideoUrl(value)) return { videoUrl: value };
  if (isImageUrl(value)) return { imageUrl: value };
  if (isAudioUrl(value)) return { audioUrl: value };
  return { prompt: value }; // text passthrough
}

Backend Orchestrator

inline-executor.ts: Same logic as frontend — reads routes, produces routeOutputs map.

output-extractor.tsgetPrimaryOutput():

case "router": {
  if (!sourceHandle) return output?.result;
  return output?.routeOutputs?.[sourceHandle];
}

input-resolver.ts: Same media-type detection as frontend.

Execution Result Shape

// Stored on node.data (frontend) / nodeStates[id].output (backend)
{
  activeRoutes: string[];                // IDs of active routes
  routeOutputs: Record<string, any>;     // routeId → input value or undefined
  result: "routed";                      // primary output marker
}

Credits

0 credits — pure control flow, no processing or API calls.

Registration Checklist

Per CLAUDE.md 18-step new node registration:

Step File What
1 backend/src/routes/ No route needed — inline execution
2 backend/src/app.ts No registration needed
3 backend/src/billing/credits.ts "router": 0 in STATIC_CREDIT_COSTS
4 backend/src/billing/credit-manager.ts "router": 0 in CREDIT_COSTS
5 frontend/src/types/nodes.ts RouterNodeData type + unions + NODE_DEFINITIONS (default outputs: 2 routes)
6 frontend/src/components/nodes/router-node.tsx Node component with interactive toggles/radios + useUpdateNodeInternals
7 frontend/src/components/nodes/index.ts nodeTypes map entry
8 frontend/src/components/editor/add-node-popup.tsx NODE_OPTIONS entry (Control Flow group)
9 frontend/src/components/editor/node-toolbar.tsx Sidebar entry (Control Flow group)
10 frontend/src/components/editor/editor-toolbar.tsx Reset case: clear result, activeRoutes, routeOutputs, status
11 frontend/src/components/editor/config-panels/processing-configs.tsx RouterConfig component
12 frontend/src/components/editor/config-panels/index.ts Export
13 frontend/src/components/editor/config-panel.tsx Import, display name, add to RUN_BUTTON_TYPES set, render conditional
14 frontend/src/lib/api.ts No API function needed — inline node
15 frontend/src/components/editor/workflow-editor/types.ts EXECUTABLE_NODE_TYPES entry
16 frontend/src/components/editor/workflow-editor/execute-node.ts Inline execution block
17 frontend/src/components/editor/workflow-editor/execution-graph.ts extractNodeOutput() case
18 frontend/src/components/editor/workflow-editor/node-input-resolver.ts Input source mapping with media-type detection

Backend orchestrator files:

Design Decisions

Decision Choice Reason
One node vs two Single Router node with mode toggle Halves registration effort, cleaner toolbar, same underlying logic
Mode naming “Radio” / “Checkbox” Universally understood, more intuitive than “Exclusive/Inclusive”
Route count User-configurable N (min 2, max 10) Flexible with hard cap to prevent layout issues
Route IDs crypto.randomUUID() Stable — deleting/re-adding never reuses IDs, protects existing edges
Canvas interaction Fully interactive toggles/radios Quick route switching without opening config panel
Radio vs checkbox visuals Distinct (circles vs toggles) Instant mode recognition at a glance
Data flow detection Auto-detect from input connectivity No extra config toggle needed — simpler UX
Execution model Inline (no API, no BullMQ job) Zero-cost control flow, instant execution
Credits 0 Pure routing, no resource consumption
Handle positioning Percentage-based relative to node height Adapts to route count without handles extending beyond node

Edge Cases

Future Considerations