| name | workflow-ui-generator |
| description | Generate React Flow workflow UIs with nodes, edges, execution orchestration, and chat interfaces. Use when creating workflow visualizations, multi-step processes, DAG systems, or orchestrated task flows. |
Workflow UI Generator
Overview
This skill helps you create complete React Flow-based workflow applications with:
- Visual workflow graphs with custom nodes and edges
- Layer-based execution using topological sorting
- Real-time status animations for node execution states
- Chat interfaces for workflow generation via LLM
- API integration for workflow planning and execution
When to Use This Skill
Activate this skill when you need to:
- Create workflow visualization UIs with React Flow
- Build multi-step process orchestrators
- Implement DAG (Directed Acyclic Graph) execution systems
- Generate workflows from natural language prompts
- Visualize task dependencies and execution flows
- Create script generation or video production workflows
Core Architecture Patterns
1. Workflow Data Model
// Node types with execution metadata
export interface WorkflowNode {
id: string;
label: string;
kind: string; // e.g., "collect", "ideate", "draft", "review"
summary?: string;
prompts?: string[];
parallelGroup?: string;
}
// Edge connections
export interface WorkflowEdge {
id: string;
source: string;
target: string;
label?: string;
}
// Execution states
export type WorkflowNodeStatus = "pending" | "running" | "completed" | "failed";
// Results with execution data
export interface WorkflowResult {
nodes: Array<WorkflowNode & {
status: WorkflowNodeStatus;
layer: number;
output?: string
}>;
edges: WorkflowEdge[];
frames: WorkflowFrame[];
}
2. Layer-Based Execution (Topological Sort)
The workflow executor uses topological sorting to determine execution order:
function buildLayers(nodes: WorkflowNode[], edges: WorkflowEdge[]): string[][] {
// Build adjacency list and track indegrees
const indegree = new Map<string, number>();
const adjacency = new Map<string, Set<string>>();
// Initialize all nodes
for (const node of nodes) {
indegree.set(node.id, 0);
adjacency.set(node.id, new Set());
}
// Build graph from edges
for (const edge of edges) {
adjacency.get(edge.source)!.add(edge.target);
indegree.set(edge.target, (indegree.get(edge.target) ?? 0) + 1);
}
// Kahn's algorithm for topological sort
const layers: string[][] = [];
let current = Array.from(nodes.map(n => n.id))
.filter(id => (indegree.get(id) ?? 0) === 0);
while (current.length > 0) {
layers.push(current);
const next = new Set<string>();
for (const nodeId of current) {
const neighbors = adjacency.get(nodeId);
if (!neighbors) continue;
for (const neighbor of neighbors) {
const nextIndegree = (indegree.get(neighbor) ?? 0) - 1;
indegree.set(neighbor, nextIndegree);
if (nextIndegree === 0) {
next.add(neighbor);
}
}
}
current = Array.from(next);
}
return layers;
}
3. React Flow Visualization
Convert workflow layers into positioned React Flow nodes:
function buildGraph(
workflow: WorkflowResult | null,
animatedStatuses: Record<string, WorkflowNodeStatus>
): { nodes: FlowNode[]; edges: FlowEdge[] } {
if (!workflow) return { nodes: [], edges: [] };
// Group nodes by layer
const layers = new Map<number, WorkflowNode[]>();
for (const node of workflow.nodes) {
const layerGroup = layers.get(node.layer) ?? [];
layerGroup.push(node);
layers.set(node.layer, layerGroup);
}
const sortedLayers = Array.from(layers.entries()).sort((a, b) => a[0] - b[0]);
const nodes: FlowNode[] = [];
const columnOffset = 320; // Horizontal spacing
const rowOffset = 180; // Vertical spacing
// Position nodes in a grid based on layers
for (const [layerIndex, layerNodes] of sortedLayers) {
const middle = (layerNodes.length - 1) / 2;
layerNodes.forEach((node, index) => {
nodes.push({
id: node.id,
position: {
x: layerIndex * columnOffset,
y: (index - middle) * rowOffset, // Center vertically
},
data: {
label: <NodeCard node={node} status={status} />,
},
});
});
}
// Create edges with animations
const edges: FlowEdge[] = workflow.edges.map(edge => ({
id: edge.id,
source: edge.source,
target: edge.target,
type: "smoothstep",
animated: animatedStatuses[edge.source] === "running",
style: { strokeWidth: 1.6 },
}));
return { nodes, edges };
}
4. Custom Node Components
Create status-aware node cards:
function NodeCard({ node, status, isActive }) {
const theme = {
pending: {
container: "border-dashed border-border/80 bg-muted/30",
badge: "bg-muted text-muted-foreground",
},
running: {
container: "border-amber-500/70 bg-amber-100/60 animate-pulse",
badge: "bg-amber-500 text-amber-900",
},
completed: {
container: "border-emerald-500/70 bg-emerald-50",
badge: "bg-emerald-500 text-emerald-950",
},
failed: {
container: "border-rose-500/70 bg-rose-50",
badge: "bg-rose-500 text-rose-50",
},
}[status];
return (
<div className={cn(
"w-64 rounded-md border bg-background p-3",
theme.container,
isActive && "ring-2 ring-offset-2"
)}>
<div className="flex items-start justify-between">
<div>
<p className="text-sm font-semibold">{node.label}</p>
{node.summary && (
<p className="text-xs text-muted-foreground">{node.summary}</p>
)}
</div>
<span className={cn("px-1.5 py-0.5 text-xs", theme.badge)}>
{status}
</span>
</div>
<div className="mt-3 text-xs text-muted-foreground">
<span>{node.kind}</span>
<span>Layer {node.layer + 1}</span>
</div>
</div>
);
}
5. Animation System
Animate status changes frame-by-frame:
// Record frames during execution
interface WorkflowFrame {
nodeId: string;
status: WorkflowNodeStatus;
note?: string;
timestamp: string;
}
// Replay frames with delays
useEffect(() => {
if (!workflow) return;
let cancelled = false;
const baseStatus = workflow.nodes.reduce((acc, node) => {
acc[node.id] = "pending";
return acc;
}, {});
setAnimatedStatuses(baseStatus);
const play = async () => {
for (const frame of workflow.frames) {
if (cancelled) return;
await delay(220); // Animation speed
setAnimatedStatuses(prev => ({
...prev,
[frame.nodeId]: frame.status,
}));
}
};
play();
return () => { cancelled = true; };
}, [workflow]);
6. Chat Interface Integration
Implement a chat UI for workflow generation:
<form onSubmit={handleSubmit}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Describe the workflow you want to create..."
/>
<button type="submit">
{creating ? <LoaderCircle className="animate-spin" /> : <SendHorizontal />}
{creating ? "Generating" : "Send"}
</button>
</form>
7. API Integration Pattern
const createPlan = async (messages: ChatMessage[]) => {
const payload = await fetch("/api/workflow", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages }),
}).then(res => res.json());
return {
reply: payload.reply,
rationale: payload.rationale,
workflow: payload.workflow,
workflowTitle: payload.workflowTitle,
executionId: payload.executionId,
};
};
Implementation Steps
Step 1: Set Up Type Definitions
Create workflow types in lib/workflows/script-orchestrator.ts:
export type WorkflowNodeKind =
| "collect"
| "ideate"
| "draft"
| "review"
| "synthesize"
| "deliver"
| (string & {});
export interface WorkflowNode {
id: string;
label: string;
kind: WorkflowNodeKind;
summary?: string;
prompts?: string[];
}
export interface WorkflowEdge {
id: string;
source: string;
target: string;
label?: string;
}
Step 2: Implement Layer Builder
Use topological sort to build execution layers (see pattern above).
Step 3: Create Workflow Executor
export async function workflowOrchestrator({ plan }: WorkflowInput) {
"use workflow";
const frames: WorkflowFrame[] = [];
const statusMap = new Map<string, WorkflowNodeStatus>();
const outputMap = new Map<string, string>();
// Initialize all as pending
for (const node of plan.nodes) {
statusMap.set(node.id, "pending");
}
const layers = buildLayers(plan.nodes, plan.edges);
// Execute layer by layer
for (const layer of layers) {
await Promise.all(
layer.map(async (nodeId) => {
const node = nodeLookup.get(nodeId);
if (!node) return;
setStatus(frames, statusMap, node.id, "running");
try {
const runner = stepRunners[node.kind] ?? fallbackRunner;
const output = await runner(node);
outputMap.set(node.id, output);
setStatus(frames, statusMap, node.id, "completed");
} catch (error) {
setStatus(frames, statusMap, node.id, "failed", error.message);
throw error;
}
})
);
}
return { nodes: enrichedNodes, edges: plan.edges, frames };
}
Step 4: Build React Flow UI
<ReactFlowProvider>
<ReactFlow
nodes={flowGraph.nodes}
edges={flowGraph.edges}
fitView
fitViewOptions={{ padding: 0.2 }}
minZoom={0.5}
maxZoom={1.5}
nodesDraggable={false}
nodesConnectable={false}
snapToGrid
>
<Background gap={24} size={3} />
<Panel position="top-right">
<WorkflowToolbar executionId={executionId} />
</Panel>
</ReactFlow>
</ReactFlowProvider>
Step 5: Add Context Providers
Create providers for workflow state management (see components/workflow/providers.tsx pattern).
Step 6: Implement Chat Interface
Add a resizable sidebar with chat history and input form.
Key Dependencies
{
"@xyflow/react": "^12.x",
"workflow": "latest",
"react": "^18.x"
}
React Flow Best Practices
- Use ReactFlowProvider: Wrap your flow in
<ReactFlowProvider>for hooks likeuseReactFlow - Memoize Node Components: Use
memo()to prevent unnecessary re-renders - Position Calculation: Calculate positions based on layers for clean layouts
- Edge Types: Use
smoothstepfor professional-looking connections - fitView Options: Add padding (0.2) for visual breathing room
- Disable Interactions: Set
nodesDraggable={false}for display-only flows - Status Animations: Use
animatedprop on edges during execution
Common Node Kinds
- collect: Gather input data or context
- ideate: Generate ideas or concepts
- draft: Create initial content
- review: Validate or critique
- synthesize: Combine multiple inputs
- deliver: Final output or export
Troubleshooting
Nodes overlap: Increase columnOffset or rowOffset values
Cycles in graph: Validate edges prevent circular dependencies
Animation too fast/slow: Adjust delay value (default: 220ms)
Edges not connecting: Ensure node IDs match edge source/target exactly
References
See reference.md for React Flow API details and examples.md for complete implementations.