Want to add some serious brainpower to your AI agents by building a visual LLM agent workflow?
In this tutorial, I’m going to walk you through the steps of creating a functional workflow builder from scratch.
We’re moving past basic text prompts and building a visual interface where you can drag, drop, and sequence the exact questions an LLM agent needs to ask.
If you’ve ever tried to explain a complex process to a new hire (or even just tried to remember the steps of a recipe while your kitchen is a mess), you know that words alone sometimes aren’t enough.
To keep our AI agents on track, especially for something high-stakes, we need a visual “source of truth.” We need a map.
That’s exactly what a workflow builder is: a map for our code.
We’ll be using React Flow (my first time!), Tailwind CSS, and Lucide Icons to make this look like a pro-level SaaS tool.
By the end of this, you’ll have a working canvas where you can click a button to append new logic nodes to a sequence. Pretty cool, right?
It’s a project that bridges the gap between high-level AI concepts and practical, human-led design.
Of course, we don’t stop at merely making it look pretty here. We’re going to dive into the “why” behind the logic, and I’ll definitely be sharing the moment I almost pulled my hair out over a state-management bug 🙈
See the Pen LLM Agent Workflow Builder| Design Technologist Screening | Klea Merkuri by Klea Merkuri (@thehelpfultipper) on CodePen.
Why This Matters
You might be thinking, “Why would I build a whole UI just to give an AI a list of questions? Can’t I just write a long prompt?”
I thought the same thing. I really did, but if re-working my portfolio’s RAG chatbot taught me anything is this: prompts are brittle.
If you’ve been following the AI boom, you’ve heard of “Agentic Workflows.” It’s a fancy term for a simple concept: instead of one giant, sprawling prompt that the AI eventually gets bored with reading, we break the AI’s job into small, manageable steps.
Think about it. If you’re an insurance call center agent, you have a script.
You ask, “What is your deductible?”
Then, “Are you in-network?”
Then, “Is prior authorization required?”
If we define these steps visually, it’s easier for humans to audit the logic. It bridges the gap between the developer (who knows React) and the person who actually knows how insurance works (the subject matter expert).
We basically move the “brain” of the operation out of the hidden prompt and into a visual map, creating a manageable, transparent process.
When you’re building something high-stakes, like an agent that talks to insurance companies to verify medical coverage, an “oops” can cost thousands of dollars 😬
In industries like healthcare, where accuracy is everything, these visual safeguards are the difference between a successful automation and a liability.
Okay, But When Would You Actually Use This?
Let’s get real: You aren’t just building this because nodes look cool (though they totally do).
You leverage a UI like this when:
1. The “Non-Coder” Needs to be the Boss
Imagine you’re building this for a medical office. The lead billing specialist knows exactly what questions to ask an insurer, but they don’t know how to write a Python script or a 2,000-word prompt.
The Leverage: With this UI, the specialist can “program” the AI by dragging boxes.
You’ve turned a complex coding task into a “Lego” set for the people who actually know the business logic.
2. The Task is Too Big for One Prompt
If an AI agent needs to stay on the phone for 20 minutes and collect 15 different data points, a single prompt will eventually “forget” its instructions (we call this context drift).
The Leverage: By using nodes, the AI only has to focus on one small task at a time.
It finishes Node A, saves the data, and then moves to Node B. This approach makes the AI more accurate because it has a smaller “mental” load.
More on conversational AI: The Ultimate Guide To Re-Engineering My Portfolio’s RAG Chatbot
3. Debugging the “Black Box”
When an AI fails in a standard chat, it’s hard to know where it went off the rails.
The Leverage: With a workflow UI, you can literally see the path the agent took 💁♀️
If it failed at the “Deductible” question, the node would turn red. You aren’t guessing anymore; you’re looking at a diagnostic map.
4. Human-in-the-Loop Oversight
Sometimes, you don’t want the AI to just keep going. You might want it to ask three questions, then stop and wait for a human to click “Approved” before it moves to the next node.
It’s kinda like your co-coding agent asking (and needing) your permission to run a terminal command.
The Leverage: This UI is the dashboard for that permission. It’s how humans and AI collaborate without the AI going rogue.
We aren’t just building a “sequence of boxes.” Instead, we’re building predictability by taking the “Black Box” of AI and putting a glass window on it so we can see (and control) exactly what it’s doing.
A workflow builder turns the AI from an unpredictable chatbot into a reliable digital employee.
Note 👇
While our current code is a linear “one-way” street, the beauty of React Flow is that it’s the foundation for branching. Eventually, you can add “If/Else” logic. If the insurer says “No,” the arrow goes to Node C; if they say “Yes,” it goes to Node D. That’s where the real power lives!
The Struggle: When “Last” Isn’t Always “Latest”
Before we jump into the code, I have to be honest with you. I underestimated something.
When I first sat down to write the logic for the “Add Node” button, I thought it would be relatively simple.
I figured I’d take the last node in the array, add 160 pixels to its Y-coordinate, and, voilà, sequence created.
But then I tried to delete a node.
Suddenly, my code was looking for the position of undefined. If I deleted every node and then tried to add a new one, the app exploded in that dramatic React console-error kind of way.
It humbles you, truly 🙈
I realized that building a “one-way” sequence builder requires a lot of safety guards.
We have to handle the “empty state” and make sure we aren’t creating “ghost edges” to nodes that don’t exist anymore.
I’ll show you exactly how I fixed that using a “single source of truth” approach. This is where your state management skills really get a workout.
Step 1: Setting Up the Canvas (The React Flow Way)
First things first, we’re using React Flow. You might see it referred to as @xyflow/react now—they recently rebranded, and it’s, apparently, one of the best libraries for building node-based UIs.
React Flow handles the “heavy lifting” of the canvas, like zooming, panning, and rendering the lines (edges) between nodes. As a first-time user, I found it to be intuitive!
Our job is to provide the data and the custom styles.
Here’s our basic setup. We need a shell to hold our header and our canvas.
import React, { useState, useCallback } from 'react';
import {
ReactFlow,
Background,
Controls,
applyNodeChanges,
applyEdgeChanges
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
// The shell that keeps everything in place
function App() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState([]);
// Boilerplate handlers for React Flow to track changes
// These use the internal helper functions to update our state arrays
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[]
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[]
);
return (
<div className="flex flex-col w-full h-screen bg-gray-50 overflow-hidden font-sans antialiased">
<header className="flex items-center justify-between px-6 py-3 bg-white border-b border-gray-200 shadow-sm shrink-0 z-10">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-purple-600 rounded-lg flex items-center justify-center text-white font-bold">T</div>
<h1 className="text-lg font-semibold text-gray-800">THT Workflow Builder</h1>
</div>
<button
onClick={handleAddNode}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-all font-medium shadow-sm active:scale-95"
>
Add Node
</button>
</header>
<div className="flex-1 w-full min-h-0">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
fitView
>
<Background color="#d1d5db" gap={20} size={1} variant="dots" />
<Controls />
</ReactFlow>
</div>
</div>
);
}To track changes, we use onNodesChange and onEdgesChange. These are crucial because React Flow needs to tell us when the user moves a node so we can update our state.
Tip 👀
Notice themin-h-0on the canvas wrapper. That’s a little CSS trick. Without it, flexbox children can sometimes act weird when they contain a high-performance canvas like React Flow. If your canvas isn’t showing up or isn’t expanding, check your parent heights and flex properties!
Step 2: Styling with Tailwind (Making it look “SaaS-y”)
React Flow nodes look a bit industrial by default. To make this feel like a modern tool, we’re creating a Custom Node.
I used Tailwind CSS to style this because I wanted total control over the typography and the purple accents.
import { Handle, Position } from '@xyflow/react';
import { MessageCircleQuestion } from 'lucide-react';
function QuestionNode({ data }) {
return (
<div className="flex items-start gap-3 p-4 w-[320px] bg-white rounded-xl border-2 border-gray-200 shadow-sm transition-all hover:border-purple-300 group">
{/* Target Handle: Where the arrow comes IN (Top) */}
<Handle
type="target"
position={Position.Top}
className="w-3.5 h-3.5 bg-purple-600 border-[2.5px] border-white shadow-sm"
/>
<div className="shrink-0 mt-0.5 text-purple-600 group-hover:scale-110 transition-transform">
<MessageCircleQuestion size={22} />
</div>
<div className="flex-1 min-w-0">
<span className="block text-[10px] font-bold text-purple-500 uppercase tracking-widest mb-1">
Insurance Question
</span>
<p className="m-0 text-[13px] font-medium text-gray-900 leading-relaxed">
{data.label}
</p>
</div>
{/* Source Handle: Where the arrow goes OUT (Bottom) */}
<Handle
type="source"
position={Position.Bottom}
className="w-3.5 h-3.5 bg-purple-600 border-[2.5px] border-white shadow-sm"
/>
</div>
);
}We’re using Handles (Source and Target) to tell React Flow where the arrows should connect. Other than that, it’s relatively self-explanatory as a component that displays the question.
Note 📝
I’m not taking system preferences into consideration for this example. We’re sticking to a clean “Light Mode” SaaS aesthetic for maximum clarity. Consistency in design helps users focus on the logic rather than the UI quirks.
Step 3: The Logic (The “Add Node” Brain)
This is where we address the “hair-pulling event” I mentioned earlier.
We need a function that finds the actual last node in the current state, calculates its position, and appends a new one without creating a mess.
const QUESTIONS = [
"What is your deductible for out-of-network services?",
"Can you confirm the patient's group number?",
"Is a prior authorization required for this procedure code?",
"What is the effective date of this policy?",
"What is the mailing address for secondary claims?"
];
let nextId = 1;
let nextQIdx = 0;
const handleAddNode = useCallback(() => {
// 1. Safety first! Check if we even have nodes.
// Don't assume nodes[nodes.length - 1] exists!
const lastNode = nodes.length > 0 ? nodes[nodes.length - 1] : null;
// 2. If no nodes exist, start at a default position.
// Otherwise, drop it 160px below the last one.
const newPosition = lastNode
? { x: lastNode.position.x, y: lastNode.position.y + 160 }
: { x: 250, y: 50 };
const newNodeId = String(nextId++);
const newNode = {
id: newNodeId,
type: "questionNode",
position: newPosition,
data: { label: QUESTIONS[nextQIdx % QUESTIONS.length] }
};
// 3. Update the nodes state
setNodes((nds) => [...nds, newNode]);
// 4. Only link them if there was a node to link FROM.
// This prevents those "ghost edges" pointing to nowhere.
if (lastNode) {
setEdges((eds) => [
...eds,
{
id: `e${lastNode.id}-${newNodeId}`,
source: lastNode.id,
target: newNodeId,
type: "smoothstep",
markerEnd: { type: "arrowclosed", color: "#7c3aed" },
style: { stroke: "#7c3aed", strokeWidth: 2 }
}
]);
}
nextQIdx++;
}, [nodes]); // If you forget [nodes] here, your logic will never see updates!I used a modulo operator (%) to cycle through a static list of insurance questions.
Tip 🤓
The modulo operator returns the remainder of the division. It ensures the index of the next node remains within the length of our question data array.
This ensures that no matter how many times you click “Add Node,” the app displays content and won’t crash when it runs out of array items!
Notice that [nodes] dependency in the useCallback. If you forget that, the function will only ever “see” the initial state of your nodes (which is likely an empty array), and your “last node” logic will be stuck in the past forever.
That’s a bug that can easily cost you precious time 😒
Adding Functional Polish with Smoothstep and Markers
One thing that surprised me during this build was how much the markerEnd edge option matters.
In React Flow, if you don’t explicitly set the marker (the arrowhead), your edges look like floating lines. It feels unfinished and confusing; you can’t tell which way the logic is supposed to flow.
By adding { type: "arrowclosed", color: "#7c3aed" }, it immediately felt like a directional workflow that the user knows exactly how to follow.
And why set type to smoothstep? Well, straight lines (the default) tend to overlap nodes when you have a vertical stack.
By setting smoothstep, we add those satisfying right-angle corners that give the whole thing a “plumbing” feel where logic flows through pipes.
Small details, but they’re what take a project from “I’m figuring this out” to “I built this” 🥂
Addressing Accessibility and Performance
As a design technologist (or even just a developer who’s responsible and cares about the user), you can’t ignore accessibility.
React Flow is great because it supports keyboard navigation out of the box. But when we build custom nodes, we need to make sure our color contrasts are high enough for WCAG standards.
That’s why I stuck with a dark purple (#7c3aed) against white. It’s punchy, professional, and very readable.
For performance, notice the useCallback on our handlers. React Flow can re-render quite often when you’re dragging nodes across a canvas.
Explore: How To Make A Fantastic Tic Tac Toe Game With React
By memoizing our functions, we ensure that we aren’t creating new function instances on every tick, keeping the canvas buttery smooth even as your workflow grows to 20 or 30 questions.
I’ve run into a similar case with interactive React applications and WebGL animations, where useMemo, useCallback, and React.memo become your best friends in making something cool perform in production settings.
Related: This Is How To Build A Smart Custom AI Chatbot
Diving Deeper into Node Customization
If we wanted to take this further, as we always kinda want to do, we could make these nodes interactive.
Imagine an <input> field inside the node where the human operator can type the question themselves.
To do that, you’d pass a change handler through the data object to the node. This is a classic React pattern: the parent App component manages the state, and the QuestionNode child component calls a function when the text changes.
If you’re following along and feel confident thus far, go ahead and implement this!
Tip 💡
When building interactive nodes, usenodragclass on your inputs. If you don’t, clicking into the text box to type might accidentally move the whole node across the screen. Talk about frustrating!
Related: How To Master React State & Event Handlers (Part 4)
Real-World Application: Bridging the Canvas to the AI
So, we have this pretty, interactive canvas. You’re clicking buttons, nodes are appearing, and everything looks “SaaS-y.”
But you might be wondering: “How does an LLM actually read a bunch of purple boxes?”
This is the part that blew my mind when I first started working with agentic workflows. We’re building a State Machine in disguise, not just a drawing tool.
When your AI agent starts a call, it needs to know its “Current State.” Instead of handing the AI a 50-page PDF of insurance rules, you hand it the data structure of this canvas.
The Logical Chain
Even without a “Save” button yet, the nodes and edges arrays in our React state are constantly tracking the “map.”
Here’s how the AI uses that map in a real insurance call:
- The Starting Node: The AI looks at the very first node in your sequence: “What percentage does the plan cover for in-network providers?”
- The Human Input: The AI asks the question, hears the answer, and records it.
- The “Next” Logic: The AI doesn’t have to guess what to do next. It looks at the Edge (the arrow) we built in Step 3. It sees that
source: Node_1leads totarget: Node_2. - The Transition: It moves its “focus” to Node 2: “What is the annual deductible amount?”
Why This Beats a Prompt
If you just put these questions in a standard text prompt, the AI might get excited and ask all four questions at once, or skip something because it got distracted by the caller’s background noise.
By using this builder, you’re creating deterministic guardrails. You are telling the AI, “You are currently at Node A. You cannot see Node C until you successfully pass through Node B.”
For those building in healthcare or high-stakes revenue cycles, this is the key moment. It takes the “black box” of an LLM and wraps it in a predictable, human-led framework.
Note 📍
In a production app, you’d eventually add auseEffector a “Save” handler to send thisnodesarray to a database (like Supabase or Firebase). That way, when the AI agent “wakes up,” it fetches this exact map you just drew.
This visual bridge is what makes modern AI systems actually usable in professional settings.
The “Delete” Problem Revisited
Remember that crash I mentioned earlier? Let’s talk about the fix because, if you want to add a delete button, you need to be careful.
React Flow provides an onNodesDelete handler. The trick is to make sure that when a node is deleted, you also clean up the edges.
Thankfully, React Flow does a lot of this automatically, but if you’re building a strict linear sequence, you might want to “re-bridge” the gap.
If you delete node 2, should node 1 connect directly to node 3?
That’s a logic puzzle for you to solve in your version!
Styling the Background and Controls
To give the project that “Infinite Canvas” feel, the Background component is your best friend.
I went with the dots variant because it’s less distracting than a full grid but still gives the user a sense of scale. Plus, it looks good 😌
<Background color="#d1d5db" gap={20} size={1} variant="dots" />
<Controls className="bg-white border border-gray-200 rounded-lg shadow-sm" />Adding the Controls component is also a must-have since it adds those zoom-in, zoom-out, and “fit to view” buttons in the corner.
If your user gets lost in the white space (which happens more than you’d think), they can just hit the “Fit View” button to center everything.
It’s an accessibility and UX win all in one.
Final Thoughts on Design Systems
Building this project really highlighted the importance of a design system.
When I used Tailwind, I was able to keep my purple accents consistent across the header, the nodes, and even the “Add” button.
Keep in mind, when you’re building tools for “delicate” industries, like healthcare or revenue cycle management, clarity is your most important design principle.
No one wants to hunt for a button when they’re trying to manage a million-dollar insurance claim.
It’s a Wrap
At the end of the day, the code is the easy part. The real challenge, and the real reward, is crafting a UX that makes designing an AI agent feel intuitive.
We aren’t just building boxes and lines; we’re building the interface where human intent meets machine execution.
As a recap, we’ve covered:
- How to set up a viewport-filling React Flow canvas.
- How to override default node styles using Tailwind.
- How to safely manage state when appending nodes to a sequence (the “Last Node” guard).
- Why directional markers and smoothstep edges matter for visual clarity.
- The “Why” behind using visual tools for LLM orchestration.
This is by no means a finished product, but it’s a rock-solid base.
Responsive behavior is an area for improvement (I did some basic responsiveness via the canvas sizing), and maybe adding a dark mode toggle would be a fun weekend addition.
Don’t forget to thoroughly test your workflow. Try deleting the middle node and see if your “Add Node” logic still knows where to put the next one. That’s the kind of edge case that separates the pros from the amateurs!
The next step for me is to use the logic I developed here in a bigger project, maybe even adding branching paths (Yes/No logic) or dynamic data fetching from an insurance API.
That’s gonna be interesting to see 🙈
Thanks for joining me on this one. It’s relentless out there in the AI world, but building tools like this makes it a lot more manageable (and a lot more fun).
See ya 👋