# Workflow Canvas

> A React workflow builder: a node editor for automations with a trigger, actions, AI steps and yes / no branches you connect and watch run.

- Type: React component (AI Kit)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add workflow-canvas`
- Docs: https://wingo-ui.com/components/workflow-canvas

## Interaction

Steps are cards on a dotted canvas, joined by rounded step lines; a condition splits into Yes and No branches with a label on each line. Drag the canvas to pan, scroll or swipe with two fingers to pan, pinch (or Ctrl and the wheel) to zoom. Click a step to select it and open its settings in a panel on the right; drag it to move it (it snaps to a 16px grid and the move is one undo step). Drag from the dot under a step to another step to connect them: a connection into a trigger, one that would make a loop or a second line from the same branch is refused and the line turns red. Drop the line on empty space and the add menu opens there. Hover a line (or select it) for its + button, which inserts a step in between; a step with nothing after it shows a dashed + stub, and a branch that goes nowhere names itself next to it. The add menu searches the step catalog by name, kind or group. Right-click a step for Duplicate, Skip and Delete, or the empty canvas to add a step where you clicked; deleting a connected step asks first and a step in the middle hands its parent to its child. The toolbar adds (after the selected step, on the selected line, or in a free spot), undoes and redoes, tidies the layout (steps glide to their new places), zooms, fits and locks, and shows Run when onRun is set. Shift and click, or Shift and a drag on the canvas, selects several steps; Delete then asks once for all of them. In run mode editing locks, the line into the running step flows, markers turn from queued to running to done or failed, the view follows the running step unless you just panned, and a run that ends with every step done pops its markers once. On a phone one finger pans and two pinch; tap a step to select it, then drag it (an unselected step never moves by accident); long-press a step for its menu or the empty canvas to add a step; handles take 44px of finger reaching away from the card; the toolbar centers above the home indicator with 44px buttons, folding tidy, zoom and lock into a menu; settings open in a sheet that stops at half height, the selected step scrolls above it, and the canvas stays usable. With a keyboard: Tab reaches the canvas, the toolbar (arrow keys move inside it) and every step and connection; Enter or Space selects, Enter again moves into the settings and Escape goes back; arrows move the selected step (Shift for 4 times as far); Delete or Backspace removes the selection; A adds a step after the selection or on the selected line; Ctrl or Cmd with Z undoes, with Shift and Z (or Y) redoes, with D duplicates and with A selects everything; + and - zoom, Shift and 1 fits; the menu key or Shift and F10 opens a step's menu. Validation problems ring the step in red and are listed in its tooltip and settings; changes are announced to screen readers.

## Installation

```bash
npx wingo-ui@latest add workflow-canvas
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/workflow-canvas` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/workflow-canvas.tsx`

npm dependencies: `npm i @xyflow/react motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Alert Dialog](https://wingo-ui.com/components/alert-dialog) ([Markdown](https://wingo-ui.com/components/alert-dialog.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Listbox](https://wingo-ui.com/components/listbox) ([Markdown](https://wingo-ui.com/components/listbox.md))
- [Popover](https://wingo-ui.com/components/popover) ([Markdown](https://wingo-ui.com/components/popover.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Switch](https://wingo-ui.com/components/switch) ([Markdown](https://wingo-ui.com/components/switch.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLongPress](https://wingo-ui.com/components/use-long-press) ([Markdown](https://wingo-ui.com/components/use-long-press.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { CalendarClock, Flag, Mail, Search, Split } from "lucide-react"
import {
  WorkflowCanvas,
  layoutWorkflow,
  type WorkflowEdge,
  type WorkflowNodeDef,
  type WorkflowValue,
} from "@/components/ui/workflow-canvas"
// the component imports @xyflow/react/dist/base.css itself; Next and Vite accept a package css import in a client component
import { AlertDialogProvider } from "@/components/ui/alert-dialog"

// the step catalog, at module scope so it keeps its identity
const nodeTypes: WorkflowNodeDef[] = [
  { type: "schedule", kind: "trigger", label: "Daily at a set time", icon: <CalendarClock />, group: "Triggers" },
  { type: "find-invoices", kind: "action", label: "Find invoices", icon: <Search />, group: "CRM actions" },
  { type: "condition", kind: "condition", label: "Condition", icon: <Split />, group: "Logic" },
  { type: "send-email", kind: "action", label: "Send email", icon: <Mail />, group: "Communication" },
  { type: "end", kind: "end", label: "End", icon: <Flag />, group: "Logic" },
]

const edges: WorkflowEdge[] = [
  { id: "e1", source: "start", target: "find" },
  { id: "e2", source: "find", target: "check" },
  { id: "e3", source: "check", target: "send", sourceHandle: "yes" },
  { id: "e4", source: "check", target: "stop", sourceHandle: "no" },
]

const initial: WorkflowValue = {
  nodes: layoutWorkflow(
    [
      { id: "start", type: "schedule", position: { x: 0, y: 0 }, data: { title: "Daily at 9:00 AM" } },
      { id: "find", type: "find-invoices", position: { x: 0, y: 0 }, data: { title: "Find overdue invoices" } },
      { id: "check", type: "condition", position: { x: 0, y: 0 }, data: { title: "Balance over $1,000?" } },
      { id: "send", type: "send-email", position: { x: 0, y: 0 }, data: { title: "Send reminder", subtitle: "Template: Payment reminder" } },
      { id: "stop", type: "end", position: { x: 0, y: 0 }, data: { title: "Nothing to send" } },
    ],
    edges,
    { nodeTypes },
  ),
  edges,
}

export function AutomationEditor() {
  const [workflow, setWorkflow] = useState(initial)
  return (
    <div className="h-[560px]">
      <WorkflowCanvas
        nodeTypes={nodeTypes}
        value={workflow}
        onValueChange={setWorkflow}
        onRun={() => console.log("Run", workflow)}
        />
      {/* once near the root: confirms deleting a connected step */}
      <AlertDialogProvider />
    </div>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `nodeTypes` | `WorkflowNodeDef[]` |  | The step catalog: { type, kind (trigger \| action \| condition \| ai \| delay \| end), label, description, icon, group, tone, outputs, defaults, keywords }. It fills the add menu (grouped by group), gives each card its icon and kind, and a condition's outputs (Yes / No unless you name your own). Keep it at module scope. |
| `value?` | `WorkflowValue` |  | The graph: { nodes: { id, type, position, data: { title, subtitle, status, error, config, disabled } }[], edges: { id, source, target, sourceHandle, label }[] }. A drag commits once, on drop; a value that arrives mid-drag waits until the drop. Steps that all sit at 0, 0 are laid out once. |
| `defaultValue?` | `WorkflowValue` |  | The starting graph while uncontrolled; an empty canvas (the trigger picker) without it. |
| `onValueChange?` | `(value: WorkflowValue) => void` |  | Every committed change: an add, a delete, a connection, a drop, a tidy, an inspector edit, an undo. |
| `selectedId?` | `string \| null` |  | The selected step, which opens the inspector. Several selected steps (Shift and click, a box) report null. |
| `defaultSelectedId?` | `string \| null` | `""` | The step selected at first while uncontrolled, so its settings open with the canvas. The playground offers the steps of the demo workflow; "" selects nothing. |
| `onSelectedIdChange?` | `(id: string \| null) => void` |  | The selection changed. With inspector="none" this is where the app opens its own panel. |
| `direction?` | `"vertical" \| "horizontal"` | `"vertical"` | The flow: top to bottom (reads best on phones) or left to right. Handles, the dashed + stubs, the label of a branch and the tidy layout follow it. |
| `readOnly?` | `boolean` | `false` | View only: pan, zoom, select and inspect. The editing tools, the + buttons and the step menu go away and the inspector shows its fields read only. |
| `showMinimap?` | `"auto" \| "always" \| "never"` | `"auto"` | auto: from 768px, while part of the graph is outside the view (after zooming in or panning). always also shows it on phones, in the top corner. The minimap pans and zooms the canvas. |
| `showControls?` | `boolean` | `true` | The floating toolbar: add, undo, redo, tidy up, zoom, fit, lock and Run. |
| `background?` | `"dots" \| "lines" \| "none"` | `"dots"` | The pattern under the steps, drawn in a faint foreground tint so it works in both themes. |
| `snapToGrid?` | `boolean` | `true` | Dragged steps land on a 16px grid, and the arrow keys move a step by it (5px without the grid). |
| `fitOnMount?` | `boolean` | `true` | Fits the whole graph into view on mount, clear of the toolbar and never above 100%, and again when a canvas that mounted hidden (a tab, a sheet) becomes visible. A flow too wide to read at that size (a long horizontal flow in a narrow column) opens at 50% from its trigger instead. |
| `inspector?` | `"auto" \| "none"` | `"auto"` | auto: the selected step's settings open in a panel on the right (at the bottom of a narrow canvas) and in a half-height sheet on phones. none: no built-in panel; show your own from onSelectedIdChange. |
| `renderInspector?` | `(node: WorkflowNode, api: { update: (data: Partial<WorkflowNode["data"]>) => void; def: WorkflowNodeDef; fields: ReactNode; readOnly: boolean }) => ReactNode` |  | The inspector body for a step: its settings form. update merges into the step's data (one undo step per 500ms burst); fields is the default name, description and skip fields; readOnly is true while read only, locked or running. |
| `onRun?` | `() => void` |  | Shows a Run button in the toolbar. Start the run, then drive running and each step's data.status. |
| `running?` | `boolean` | `false` | Run mode: editing locks, steps show node.data.status as a marker, the line into a running step flows, finished lines turn green, and the view follows the running step unless the person panned in the last 3 s. |
| `confirmDelete?` | `boolean` | `true` | Asks before deleting a step that has connections (through useConfirm, so mount AlertDialogProvider once). |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The selection ring, the dragged connection, a flowing line, the add button and the + buttons. |
| `color?` | `string` | `""` | Any CSS color; replaces the tone. Leave empty to use the tone. |
| `labels?` | `Partial<WorkflowCanvasLabels>` |  | Every string, English by default (DEFAULT_WORKFLOW_CANVAS_LABELS): canvas, roleDescription, step, instructions, the toolbar's names (addStep, undo, redo, tidy, zoomIn, zoomOut, fit, lock, unlock, more, run, running), searchSteps, noSteps, emptyTitle, emptyDescription, insert, yes, no, the inspector's (inspector, title, subtitle, disabled, duplicate, delete, issuesTitle), deleteTitle(title), deleteManyTitle(count), deleteDescription, kinds, status, issues (the validation messages), announce, nodeLabel(step) and edgeLabel(from, to, label). |
| `classNames?` | `Partial<Record<"flow" \| "toolbar" \| "minimap" \| "menu" \| "inspector" \| "header" \| "body" \| "footer" \| "empty" \| "node" \| "edgeLabel" \| "insert", string>>` |  | Extra classes for the parts. The root takes className; drop its frame with className="rounded-none bg-transparent". |
| `flowProps?` | `Partial<ReactFlowProps<WorkflowFlowNode, WorkflowFlowEdge>>` |  | Extra props for the underlying ReactFlow element (minZoom, maxZoom, zoomOnScroll, proOptions...). The xyflow attribution is hidden by default, which its MIT license allows; pass proOptions to show it. |
| `className?` | `string` |  | Classes for the root, merged last. It fills its parent (at least 420px tall) in a rounded-3xl bg-card frame; give the parent a height. |

## Parts

### WorkflowNodeCard (component)

The look of one step on its own: the kind's icon tile, the kind, the title, the subtitle and the run marker, with the selected, invalid and skipped looks. For previews, template galleries and lists. Props: { node?: WorkflowNode; def?: WorkflowNodeDef; title?; subtitle?; kind?; icon?; status?; selected?; disabled?; invalid?; tone?; color?; labels?; classNames? }.

### layoutWorkflow (function)

A layered layout: ranks by the longest path from the trigger, a condition's branches side by side in output order, each row spread evenly around its parents. Returns the steps with new positions (top-left at 0, 0). Signature: (nodes: WorkflowNode[], edges: WorkflowEdge[], options?: { direction?; gapX? = 64; gapY? = 56; nodeWidth? = 260; nodeHeight? = 64; sizes?; nodeTypes? }) => WorkflowNode[].

### validateWorkflow (function)

The problems that stop an automation from running: no trigger or more than one, a connection into a trigger, loops, a condition branch that goes nowhere, unknown step types and connections to missing steps. Signature: (value: WorkflowValue, nodeTypes: WorkflowNodeDef[], messages?: Partial<WorkflowIssueMessages>) => { nodeId?: string; edgeId?: string; message: string }[].

### createWorkflowNode (function)

A new step from its definition, with a fresh id and the def's defaults (call it in handlers, not while rendering). Signature: (def: WorkflowNodeDef, position?: { x: number; y: number }) => WorkflowNode.

### DEFAULT_WORKFLOW_CANVAS_LABELS (constant)

The English strings, to spread under your translations. Type: WorkflowCanvasLabels.

### DEFAULT_WORKFLOW_ISSUE_MESSAGES (constant)

The English validation messages that validateWorkflow and the canvas use: noTrigger, multipleTriggers, triggerInput, cycle, missingNode, unknownType(type), unconnectedOutput(output), plus the "Yes" / "No" branch names. Pass a partial copy to validateWorkflow or as labels.issues to translate them. Type: WorkflowIssueMessages.

### WorkflowValue (type)

The automation: { nodes, edges }. A node is { id, type, position, data: { title, subtitle?, status?, error?, config?, disabled? } }; an edge is { id, source, target, sourceHandle?, label? }, where sourceHandle names a condition's output.

### WorkflowNodeDef (type)

One step type for the add menu: { type, kind, label, description?, icon, group?, tone?, outputs?, defaults?, keywords? }. kind is trigger, action, condition, ai, delay or end.

## Examples

- **Horizontal, brand tone**: direction="horizontal" for a left to right flow; tone="brand" colors the selection, the add button and the + buttons.
- **Empty**: No steps yet: the canvas offers the triggers from nodeTypes, and only while there is none.
- **Read-only run history**: readOnly with node.data.status from a finished run; pan, zoom and inspect, nothing moves.
- **Your own inspector**: inspector="none" with selectedId and onSelectedIdChange: the app shows the step in its own side panel.
- **On a phone**: At 390px the toolbar centers with 44px buttons and the settings open in a sheet that stops at half height.
- **Step cards on their own**: WorkflowNodeCard for previews and template lists: every kind, running, failed, skipped and selected.

## See also

- [Rule Builder](https://wingo-ui.com/components/rule-builder) ([Markdown](https://wingo-ui.com/components/rule-builder.md))
- [Tool Call Timeline](https://wingo-ui.com/components/tool-call-timeline) ([Markdown](https://wingo-ui.com/components/tool-call-timeline.md))
- [Kanban](https://wingo-ui.com/components/kanban) ([Markdown](https://wingo-ui.com/components/kanban.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Popover](https://wingo-ui.com/components/popover) ([Markdown](https://wingo-ui.com/components/popover.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))

---

Canonical: https://wingo-ui.com/components/workflow-canvas
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
