Workflow Editor
The Workflow Editor is how admins build automated multi-step processes: enrollment flows, email sequences, decision branches. It is a visual editor with a drag-and-drop canvas. Users pick node types (Form, Email, Decision, Wait) from a left palette, drop them onto the canvas, connect them to define the order, and configure each step in a right-side panel. Built on React Flow for the canvas and node graph, with MUI components for all the chrome. This is a unique full-screen layout. It does not use standard content padding.
Overview
The Workflow Editor lets admins build automated multi-step processes by arranging nodes on a canvas. Each node type (Form, Email, Decision, Wait, Markdown) has its own configuration. Nodes connect via drag-and-drop edges.
Admin needs to define a multi-step automated process with branching logic
Drag nodes from palette, connect on canvas, configure in side panel
React Flow for canvas, MUI for chrome and controls
Interactive Preview
Workflow Editor
Form
Collect user input through a configured form template.
Send an automated email using a saved template.
Decision
Branch the workflow based on a true/false condition.
Manual Decision
Pause the workflow and wait for an admin to approve or decline before continuing.
Form
Student Application
Decision
Eligible?
Manual Decision
Application Review
Decision Explainer
Welcome Email
Form
Course Selection
n2Node Types
Each node type represents a step in a workflow. Below is the full catalog: every node's canvas appearance, default settings, and the configurable fields shown in the properties panel when selected.
Form
Collect user input through a configured form template.
Form
New Form
Form template
templateRequired
requiredtemplate: "Application" required: true
Send an automated email using a saved template.
New Email
Template
templateRecipient
recipienttemplate: "Welcome"
recipient: "{{user.email}}"Decision
Branch the workflow based on a true/false condition.
Decision
New Decision
Condition expression
conditionYes branch label
yesLabelNo branch label
noLabelcondition: "user.age >= 18" yesLabel: "Yes" noLabel: "No"
Manual Decision
Pause the workflow and wait for an admin to approve or decline before continuing.
Manual Decision
Application Review
Decision name
decisionNameContext variables
contextVariablesEnd-user message
endUserMessagedecisionName: "" contextVariables: user.full_name,user.email,application.id endUserMessage: "Your application is being reviewed. We will notify you once a decision has been made."
Wait
Pause the workflow for a fixed duration before continuing.
Wait
Wait
Duration
durationUnit
unitduration: 1 unit: "days"
Markdown
Display formatted text or instructions inline in the workflow.
Markdown
Heading
Content
contentAlignment
aligncontent: "Welcome to the workflow" align: "left"
Palette Items
The left panel renders one draggable card per node type. Each item shows a drag handle, the type icon, the type name, and a short description.
Form
Collect user input through a configured form template.
Send an automated email using a saved template.
Decision
Branch the workflow based on a true/false condition.
Panel width
240pxItem padding
12pxItem gap
8pxBorder
1px soliddividerBorder radius
8pxradiusMdBackground
background.paperHover border
1px solidprimaryDrag handle
DragIndicatorIcon16px, gray500Type icon
20pxcolor per node typeTitle
14px600 weight, navyDescription
12pxgray500Inner gap
12pxCursor
grabgrabbingDrag API
HTML5 drag-and-drop, React Flow onDropCanvas Node States
Nodes on the canvas transition between states based on user interaction. Clicking a node selects it and opens the properties panel. Selected nodes show a CTA when they require configuration.
Form
No form selected
Form
Application Form
Width
180pxRadius
10pxradiusLgShadow
card tokenIcon
16pxnode-type colorTitle
14px600 weight, navySubtitle
12pxgray500Handle size
10px circlecentered on edgeConnected Nodes
Nodes connect via source (right) and target (left) handles in a horizontal flow. Edges are drawn as lines between handles. Decision nodes support multiple outputs for branching logic.
Flow direction
horizontal (left to right)Edge line
2px solidgray300Edge (selected)
2px solidprimarySnap to grid
24px gridProperties Panel
Appears on the right when a node is selected. Shows editable properties, read-only metadata, and an expand option for complex editors (Form Editor, Email Template Editor).
node_f7a2b3c1Panel width
280–320pxTrigger
Appears on node select, hides on deselectValidation States
The workflow editor validates in real-time. Invalid nodes show visual indicators. The save button is disabled until all errors are resolved. Orphaned (unreachable) nodes show warnings.
Form
Application Form
Form
Missing required fields
Send Reminder
No incoming connections
Invalid node border
2px solid #EF4444dangerError indicator
ErrorIcontop-right of node, danger colorInvalid connection
red flash on handle + preventedHeader Kebab Menu
The 3-dot menu in the header strip contains secondary and destructive actions. This keeps the bottom bar focused on the primary save/cancel flow and prevents accidental destructive clicks.
Separation of concerns
Destructive actions (delete) are behind an extra click, away from save/cancel.
Follows existing pattern
Matches the 3-dot context menu used on list cards throughout the app.
Extensible
Secondary actions like duplicate and export can be added here without cluttering the header.
Test Mode
Admins can switch from Edit to Test mode to simulate workflow execution. Nodes are highlighted step-by-step as the test runs. Variable values can be simulated, and branch paths can be manually selected at Decision nodes.
Test Mode Active
Form
Decision
Mode toggle
Edit ↔ Testsegmented control in headerActive indicator
amber banner below header when in test modeVariable Management
Workflows maintain a set of variables. Some are global to the workflow, others are generated by individual nodes. Each node accesses data from previous nodes in the execution chain. Variables use a reference syntax for dynamic values in node configurations.
Variables use {{node_name.field}} syntax
Global
Process-level variables available to all nodes (e.g. process ID, admin user)
Node output
Data generated by a node, available to all subsequent nodes in the chain
Context
Runtime data from the parent process (e.g. student record, cohort info)
Interaction Flow
Entry
User opens workflow editor from a process. Breadcrumb shows full path. Node palette is visible on the left.
Drag to add
User drags a node type from the palette onto the canvas. A new node appears with connection handles.
Select node
Clicking a node highlights it with 2px orange border + primaryLight background. Properties panel appears on the right.
Configure
User edits node settings in the properties panel. Form nodes can open the Form Editor. Changes reflect live.
Connect nodes
User drags from a source handle to a target handle to create edges. Decision nodes create branching paths.
Save / Delete
Bottom bar: "Save" persists the workflow, "Cancel" discards changes. "Delete workflow" is in the header kebab menu (3-dot) to separate destructive from constructive actions.
Component Map
Breadcrumb bar
Breadcrumbs52px topbarHeader strip
Boxtitle input + description + status selectHeader kebab menu
Menu + MenuItemDelete workflow (danger item)Bottom actions
Buttonoutlined Cancel + contained SaveConstraints & Notes
Known limitations and platform requirements for v1 of the Workflow Editor.
Platform
Desktop only, no mobile supportAccess
Admins and super-admins onlyPerformance
Canvas slows with 200+ nodesVersioning
Not supported in v1, no history or rollbackConcurrent editing
Not supported, one editor at a timeData format
JSON serialization for workflow graphsLibrary
React Flowprimary canvas libraryCode
function FormNode({ data, selected }) {
return (
<Box
sx={{
border: selected ? '2px solid' : '1px solid',
borderColor: selected ? 'primary.main' : 'grey.200',
backgroundColor: selected ? 'primary.light' : 'white',
borderRadius: '10px',
p: 2,
width: 180,
}}
>
<Handle type="target" position={Position.Top} />
<Stack direction="row" alignItems="center" gap={1}>
<DescriptionIcon sx={{ fontSize: 16, color: 'primary.main' }} />
<Typography variant="body2" fontWeight={600}>Form</Typography>
</Stack>
<Typography variant="caption" color="text.secondary">
{data.label || 'No form selected'}
</Typography>
<Handle type="source" position={Position.Bottom} />
</Box>
);
}<Box
draggable
onDragStart={(e) => {
e.dataTransfer.setData('application/reactflow', nodeType);
e.dataTransfer.effectAllowed = 'move';
}}
sx={{
display: 'flex', alignItems: 'center', gap: 1.5,
p: 1.5, border: '1px solid', borderColor: 'divider',
borderRadius: '8px', cursor: 'grab',
'&:hover': { borderColor: 'primary.main' },
}}
>
<DragIndicatorIcon sx={{ fontSize: 16, color: 'grey.500' }} />
<DescriptionIcon sx={{ fontSize: 20, color: 'primary.main' }} />
<Box>
<Typography variant="body2" fontWeight={600}>Form</Typography>
<Typography variant="caption" color="text.secondary">
Collect user input and data
</Typography>
</Box>
</Box>import { Handle, Position } from '@xyflow/react';
import { tokens } from '@/theme/theme';
// Place inside a React Flow node component:
<Handle
type="target"
position={Position.Left}
style={{
background: '#FFFFFF',
width: 11,
height: 11,
border: `1px solid ${tokens.color.gray400}`,
boxShadow: '0 1px 2px rgba(0,0,0,0.12)',
}}
/>// Decision nodes have two outputs: yes and no.
// Each handle needs a unique id so edges can target it.
<Handle
type="source"
id="yes"
position={Position.Right}
style={{
top: '30%',
background: '#FFFFFF',
width: 11,
height: 11,
border: `1px solid ${tokens.color.success}`,
}}
/>
<Handle
type="source"
id="no"
position={Position.Right}
style={{
top: '70%',
background: '#FFFFFF',
width: 11,
height: 11,
border: `1px solid ${tokens.color.danger}`,
}}
/>// Edge object passed to React Flow:
const edges = [
{
id: 'e-decision-yes',
source: 'decision',
sourceHandle: 'yes',
target: 'email-welcome',
style: {
stroke: tokens.color.gray400,
strokeWidth: 2,
},
label: 'Yes',
labelStyle: {
fontSize: 11,
fontWeight: 600,
fill: tokens.color.successDark,
},
labelBgStyle: {
fill: tokens.color.successLight,
},
labelBgPadding: [6, 4],
labelBgBorderRadius: 9,
markerEnd: { type: 'arrowclosed', color: tokens.color.gray400 },
},
];