By Sagar Shankaran, Founder of CallSphere
Create a visual agent workflow editor using React, drag-and-drop libraries, and a node-based canvas. Learn node rendering, connection drawing, and workflow serialization.
Key takeaways
Not every agent designer is a developer. Product managers, domain experts, and operations teams need to define agent workflows — which tools to call, when to escalate, how to route conversations — without writing code. A visual workflow editor lets them drag agent nodes onto a canvas, connect them with edges, and configure behavior through form panels. React Flow is the dominant library for building these interfaces in React.
React Flow provides the canvas, node rendering, edge drawing, and interaction handling. Install it and create a basic workflow editor.
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
import {
ReactFlow,
Background,
Controls,
MiniMap,
Node,
Edge,
useNodesState,
useEdgesState,
addEdge,
Connection,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
const initialNodes: Node[] = [
{
id: "triage",
type: "agentNode",
position: { x: 250, y: 50 },
data: { label: "Triage Agent", model: "gpt-4o" },
},
{
id: "support",
type: "agentNode",
position: { x: 100, y: 250 },
data: { label: "Support Agent", model: "gpt-4o-mini" },
},
{
id: "billing",
type: "agentNode",
position: { x: 400, y: 250 },
data: { label: "Billing Agent", model: "gpt-4o-mini" },
},
];
const initialEdges: Edge[] = [
{ id: "e1", source: "triage", target: "support", label: "support" },
{ id: "e2", source: "triage", target: "billing", label: "billing" },
];
function WorkflowEditor() {
const [nodes, setNodes, onNodesChange] =
useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] =
useEdgesState(initialEdges);
const onConnect = (connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
};
return (
<div className="w-full h-[700px] border rounded-xl">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
</div>
);
}
Default nodes are plain rectangles. Create custom nodes that display agent information with connection handles for inputs and outputs.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
import { Handle, Position, NodeProps } from "@xyflow/react";
interface AgentNodeData {
label: string;
model: string;
}
function AgentNode({ data }: NodeProps) {
const nodeData = data as unknown as AgentNodeData;
return (
<div className="bg-white border-2 border-blue-200 rounded-xl
shadow-md px-4 py-3 min-w-[180px]">
<Handle
type="target"
position={Position.Top}
className="w-3 h-3 bg-blue-500"
/>
<div className="flex items-center gap-2 mb-1">
<div className="w-8 h-8 bg-blue-100 rounded-lg
flex items-center justify-center text-lg">
A
</div>
<div>
<p className="font-semibold text-sm">{nodeData.label}</p>
<p className="text-xs text-gray-500">{nodeData.model}</p>
</div>
</div>
<Handle
type="source"
position={Position.Bottom}
className="w-3 h-3 bg-blue-500"
/>
</div>
);
}
const nodeTypes = { agentNode: AgentNode };
The Handle components define connection points. target handles accept incoming edges, source handles start outgoing edges. Position them at the top and bottom for a top-to-bottom flow layout.
A sidebar palette lets users drag new node types onto the canvas. Use React DnD or the native HTML drag API.
const nodeTemplates = [
{ type: "agentNode", label: "Agent", icon: "A" },
{ type: "toolNode", label: "Tool", icon: "T" },
{ type: "conditionNode", label: "Condition", icon: "?" },
{ type: "outputNode", label: "Output", icon: "O" },
];
function NodePalette() {
const onDragStart = (
event: React.DragEvent,
nodeType: string
) => {
event.dataTransfer.setData(
"application/reactflow",
nodeType
);
event.dataTransfer.effectAllowed = "move";
};
return (
<div className="w-48 border-r p-4 space-y-2">
<h3 className="font-semibold text-sm mb-3">Components</h3>
{nodeTemplates.map((tpl) => (
<div
key={tpl.type}
draggable
onDragStart={(e) => onDragStart(e, tpl.type)}
className="flex items-center gap-2 p-2 border rounded-lg
cursor-grab hover:bg-gray-50"
>
<span className="w-7 h-7 bg-gray-100 rounded flex
items-center justify-center text-sm">
{tpl.icon}
</span>
<span className="text-sm">{tpl.label}</span>
</div>
))}
</div>
);
}
When a node is dropped on the canvas, calculate its position relative to the React Flow viewport and add it to the nodes array.
import { useReactFlow } from "@xyflow/react";
function useDropHandler(
setNodes: React.Dispatch<React.SetStateAction<Node[]>>
) {
const { screenToFlowPosition } = useReactFlow();
const onDrop = (event: React.DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData("application/reactflow");
if (!type) return;
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode: Node = {
id: crypto.randomUUID(),
type,
position,
data: { label: `New ${type}`, model: "gpt-4o-mini" },
};
setNodes((nds) => [...nds, newNode]);
};
const onDragOver = (event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
};
return { onDrop, onDragOver };
}
The workflow must be saved to a backend. Serialize the nodes and edges into a JSON structure that your agent runtime can interpret.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
interface SerializedWorkflow {
version: string;
nodes: Array<{
id: string;
type: string;
config: Record<string, unknown>;
}>;
edges: Array<{
source: string;
target: string;
condition?: string;
}>;
}
function serializeWorkflow(
nodes: Node[],
edges: Edge[]
): SerializedWorkflow {
return {
version: "1.0",
nodes: nodes.map((n) => ({
id: n.id,
type: n.type || "agentNode",
config: n.data as Record<string, unknown>,
})),
edges: edges.map((e) => ({
source: e.source,
target: e.target,
condition: e.label as string | undefined,
})),
};
}
Listen for the onNodeClick event on the ReactFlow component. Store the selected node ID in state and conditionally render a side panel with form fields for that node's configuration (model, system prompt, tools, temperature). Update the node's data in the nodes array when the form changes.
Check that all nodes have at least one incoming or outgoing edge (except the start node). Verify there are no cycles if your agent runtime does not support them. Ensure every condition edge has a non-empty label. Run these validations before serialization and highlight invalid nodes with a red border.
Yes. Maintain a history stack of { nodes, edges } snapshots. Push a new snapshot on every meaningful change (node add, delete, move, connect). Pop from the stack on undo. Use a separate redo stack that gets cleared when a new change is made after an undo.
#DragAndDrop #VisualEditor #ReactFlow #TypeScript #WorkflowBuilder #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
AI SDK 5 ships fully typed chat for React, Svelte, Vue, and Angular plus first-class agent loop primitives. Here are the patterns that matter for shipping in 2026.
Mastra.ai is becoming the go-to TypeScript agent framework in 2026. Workflows, RAG, evals, and an honest comparison with Vercel AI SDK 5 for serious teams.
Convex's reactive queries auto-push every transcript update to every subscribed client. Real working code for actions, mutations, and OpenAI Realtime streaming.
The MCP TypeScript SDK 1.0 makes server development straightforward and type-safe. A walkthrough from scaffold to a deployed Cloud Run service ready for production traffic.
AI SDK 5 ships agent loops, MCP client, and Generative UI v2 — the cleanest TypeScript stack for shipping AI features in 2026 without rewriting your frontend.
Bun's native Bun.serve WebSocket API outperforms Node ws by 4-7x on idle sockets. Build a full Realtime voice bridge in TypeScript with binary frames and pub/sub.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.