# Workflow

> A node canvas for chaining steps into an AI workflow and watching it run.

Canonical: https://hextaui.com/blocks/workflow
Last updated: 2026-10-29

## Overview

AI workflows are easiest to trust when you can see them. Workflow lays the steps out on a canvas: a trigger, the models that read and write, the tools they call, the conditions and loops between them and the people who approve the risky parts. Each card names the step, shows what it’s set to and flags what’s missing, so a run can’t start until the workflow is complete.

Press Run to try it with test input. Steps light up as they start, a highlight travels along each connection as work moves forward, and branches that weren’t taken fade out. A run log lists every step with its timing, tokens, input and output. Steps that wait for a person show Approve and Reject on the card, in the log and in the floating status bar, and a failed step offers Retry from here, which reuses the finished steps before it.

The canvas is a soft gray sheet of white cards. Each card has a tinted icon tile, the step’s name and type, a status pill that always names the state, and a sunken well that holds the settings when idle, what the step is doing while it runs and a preview of its output when it’s done. Conditions and loops list their branches as rows inside the card, with each connection leaving from its row. Connections are rounded blue lines that dim on paths the run didn’t take, and a short dash travels along the one doing work. The final step lights a soft pastel rainbow shadow under its bottom edge while it works, a selected step gets a blue ring and a softer version of the same shadow and lifts the connections around it, and green, amber and red only mean done, waiting and failed. On phones, and for screen readers, a List view shows the same workflow as an ordered list you can edit, run and approve from.

## Installation

### Step 1: Get HextaUI Pro

One payment unlocks every block, including future ones. [See pricing](https://hextaui.com/pricing).

### Step 2: Add the Pro registry to components.json

```json title="components.json"
{
  "registries": {
    "@hextaui-pro": {
      "url": "https://hextaui.com/r/pro/{name}.json",
      "headers": {
        "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
      }
    }
  }
}
```

### Step 3: Add your token

Create a token on your [account page](https://hextaui.com/account) and put it in `.env.local` as `HEXTAUI_PRO_TOKEN`.

### Step 4: Add the block

```bash
npx shadcn@latest add @hextaui-pro/workflow
```

## Usage

### Saving and loading

Keep the graph in state with onChange and save it a moment after edits stop. serializeWorkflow writes a versioned JSON file with rounded positions, and parseWorkflow reads one back, throwing a readable error for anything it can’t load.

```tsx
"use client"

import * as React from "react"
import { IconDownload, IconUpload } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { toast } from "@/components/ui/toast"

import { Workflow, parseWorkflow, serializeWorkflow, type WorkflowGraph } from "@/components/blocks/workflow/workflow"

export function WorkflowEditor({ id, initial }: { id: string; initial: WorkflowGraph }) {
  const [graph, setGraph] = React.useState(initial)
  const fileRef = React.useRef<HTMLInputElement>(null)
  const saved = React.useRef(serializeWorkflow(initial))

  React.useEffect(() => {
    const json = serializeWorkflow(graph)
    if (json === saved.current) return
    const timer = setTimeout(async () => {
      const response = await fetch(`/api/workflows/${id}`, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: json,
      })
      if (response.ok) saved.current = json
      else toast.error("Couldn’t save the workflow")
    }, 800)
    return () => clearTimeout(timer)
  }, [graph, id])

  const download = () => {
    const blob = new Blob([serializeWorkflow(graph, { name: id })], { type: "application/json" })
    const link = document.createElement("a")
    link.href = URL.createObjectURL(blob)
    link.download = `${id}.workflow.json`
    link.click()
    URL.revokeObjectURL(link.href)
  }

  const upload = async (file: File) => {
    try {
      setGraph(parseWorkflow(await file.text()))
    } catch (error) {
      toast.error("Couldn’t open that file", { description: (error as Error).message })
    }
  }

  return (
    <div className="flex h-dvh flex-col">
      <div className="flex justify-end gap-2 border-b p-2">
        <Button variant="outline" size="sm" onClick={() => fileRef.current?.click()}>
          <IconUpload data-icon="inline-start" />
          Import
        </Button>
        <Button variant="outline" size="sm" onClick={download}>
          <IconDownload data-icon="inline-start" />
          Export
        </Button>
        <input
          ref={fileRef}
          type="file"
          accept="application/json,.json"
          hidden
          onChange={(event) => {
            const file = event.target.files?.[0]
            if (file) void upload(file)
            event.target.value = ""
          }}
        />
      </div>
      <Workflow name="Support triage" nodes={graph.nodes} edges={graph.edges} onChange={setGraph} />
    </div>
  )
}
```

### Streaming a run from your server

onRun is an async generator. Post the graph to your server and pass the response to readRunEvents, which reads server-sent events or one JSON event per line. Send step events such as {"type":"step","nodeId":"classify","status":"done","output":{…}} and end with a run event. onApprove sends a person’s answer back while the run waits.

```tsx
"use client"

import { Workflow, readRunEvents, type WorkflowGraph, type WorkflowRunHandler } from "@/components/blocks/workflow/workflow"

const run: WorkflowRunHandler = async function* ({ runId, graph, input, from, signal }) {
  const response = await fetch("/api/workflows/run", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ runId, graph, input, from: from && { nodeId: from.nodeId, runId: from.run.id } }),
    signal,
  })
  yield* readRunEvents(response)
}

async function approve({ runId, nodeId, approved }: { runId: string; nodeId: string; approved: boolean }) {
  const response = await fetch(`/api/workflows/runs/${runId}/approvals`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ nodeId, approved }),
  })
  if (!response.ok) throw new Error(await response.text())
}

export function LiveWorkflow({ graph }: { graph: WorkflowGraph }) {
  return (
    <Workflow
      name="Support triage"
      nodes={graph.nodes}
      edges={graph.edges}
      onRun={run}
      onApprove={approve}
      defaultInput="My calendar stopped syncing this morning."
    />
  )
}
```

### Read-only embed

Set readOnly to show a workflow without the toolbar, editing or approvals, for docs, run reports and shared links. Pass a stored run to show how it went. Scrolling passes through to the page, and ⌘ or Ctrl with the wheel zooms.

```tsx
import { Workflow, type WorkflowGraph, type WorkflowRun } from "@/components/blocks/workflow/workflow"

export function WorkflowPreview({ graph, run }: { graph: WorkflowGraph; run?: WorkflowRun }) {
  return (
    <div className="h-96 overflow-hidden rounded-xl border">
      <Workflow readOnly nodes={graph.nodes} edges={graph.edges} run={run} defaultView="canvas" />
    </div>
  )
}
```

## Anatomy

The parts you compose, from the outside in.

| Part | Description |
| --- | --- |
| `Workflow` | The editor: toolbar, canvas or list, side panel and status bar. Holds history, selection and the current run. |
| `Canvas` | The node graph built on React Flow. It opens at 100% on the trigger, or fits the whole workflow when it’s small enough, with zoom controls, an optional minimap and snapping. |
| `Step card` | One step: an icon tile, title and type, a status pill, and a sunken well with its settings, live activity or output preview. Timing and tokens sit under the title during a run, and branching steps add one row per output. |
| `List view` | The same step cards stacked in a tray in run order, with branches as rows that link to their targets. The default on narrow screens and the screen-reader alternative to the canvas. |
| `Step settings` | Fields for the selected step, generated from its type, with Duplicate and Delete. |
| `Run log and history` | Every step of the current run with timing, tokens, input and output, and a list of past runs to open again. |
| `Status bar` | Floats over the canvas during a run with progress, the current step, elapsed time and Approve or Retry. |

## API reference

### Workflow

Also accepts every div prop except onChange.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `nodes` | `WorkflowNode[]` | – | Steps with an id, a type, a position and their settings in data. data.title names the step. |
| `edges` | `WorkflowEdge[]` | – | Connections. sourceHandle picks the branch, such as "true" or "false" on a condition. |
| `onChange` | `(graph: WorkflowGraph) => void` | – | Called after every edit, including undo and redo. Moves are reported once a drag ends. |
| `nodeTypes` | `WorkflowNodeTypes` | `defaultNodeTypes` | The steps people can add: trigger, model, tool, condition, loop, approval and output. Spread it to add your own. |
| `onRun` | `WorkflowRunHandler` | – | An async generator that yields run events. Shows Run when set. Receives the graph, input, an AbortSignal for Stop, from when retrying, and approval(nodeId) to wait for a person. |
| `onApprove` | `(approval: WorkflowApproval) => void \| Promise<void>` | – | Called when someone approves or rejects. A rejected promise keeps the step waiting and shows a toast. |
| `onRunComplete` | `(run: WorkflowRun) => void` | – | Called once a run succeeds, fails or is stopped. Store it and pass it back through runs. |
| `runs` | `WorkflowRun[]` | – | Past runs for the History tab. |
| `run` | `WorkflowRun \| null` | – | Shows a run you control instead of the live one, for example one loaded from your database. |
| `readOnly` | `boolean` | `false` | Hides the toolbar and turns off editing and approvals. |
| `name` | `string` | `"Untitled workflow"` | Shown in the toolbar. |
| `defaultInput` | `string` | – | Fills the test input in the Run popover. |
| `defaultView` | `"canvas" \| "list"` | – | The view to start in. By default the list under 768px wide and the canvas above. |
| `minimap` | `boolean` | `false` | Shows the minimap at first. People can toggle it from the zoom controls. |

### WorkflowNodeType

One entry in nodeTypes.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | – | The step’s name in menus and on cards. |
| `description` | `string` | – | One line shown when adding the step. |
| `icon` | `ReactNode` | – | Shown beside the step’s title. Use a 16px line icon. iconFor(data) can change it per step. |
| `accent` | `string` | – | A text colour class for the icon tile, like text-chart-4. Muted when left out. |
| `activity` | `string` | – | The verb shown in the card while the step runs, like Thinking or Calling. Defaults to Working. |
| `sublabel` | `(data) => string` | – | The line under the title. Defaults to the type’s label. |
| `group` | `string` | – | Groups steps in the add menu. |
| `fields` | `WorkflowField[]` | – | text, textarea, select or slider fields for the settings panel. required fields block Run until filled, and when(data) shows a field only when it applies. |
| `defaults` | `WorkflowData` | – | Settings for a new step. |
| `input` | `boolean` | `true` | Set false for triggers, which start the workflow and take no input. |
| `outputs` | `{ id, label? }[]` | `[{ id: "out" }]` | One per branch. An empty array ends the workflow. |
| `repeats` | `boolean` | – | Lets connections loop back through this step. |
| `summary` | `(data: WorkflowData) => string` | – | The line under the title, such as “Hexta 2 · temp 0.2”. |

### WorkflowRunEvent

What onRun yields. Missing at values use the time the event arrives.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `{ type: "step" }` | `{ nodeId, status, input?, output?, error?, tokens?, message?, at? }` | – | status is queued, running, waiting, done, failed or skipped. message is the question shown while waiting. |
| `{ type: "run" }` | `{ status, runId?, error?, at? }` | – | status is running, waiting, succeeded, failed or cancelled. Ending the run settles any steps still open. |

### Helpers

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `serializeWorkflow(graph, meta?)` | `string` | – | A versioned JSON file for saving or exporting. |
| `parseWorkflow(json)` | `WorkflowGraph` | – | Reads a saved file. Throws an error that names the broken node or edge. |
| `readRunEvents(response)` | `AsyncGenerator<WorkflowRunEvent>` | – | Reads a fetch response of server-sent events or JSON lines, and throws the server’s message if the request failed. |
| `validateWorkflow(graph, types)` | `WorkflowIssue[]` | – | What blocks a run, such as a missing trigger, a step with no input or an empty required field. |
| `layoutWorkflow(graph, types, options?)` | `WorkflowNode[]` | – | Positions steps left to right by depth. Arrange steps uses it. |
| `applyRunEvent(run, event)` | `WorkflowRun` | – | The reducer the editor uses, for building runs on your server. |

## Keyboard

| Key | Action |
| --- | --- |
| `Tab` | Moves between steps, connections and controls. |
| `Enter` | Opens the focused step’s settings, or its log during a run. |
| `↑` `↓` `←` `→` | Moves the selected steps. Hold Shift to move further. |
| `N` | Adds a step after the selected one, or in the middle of the view. |
| `⌫` | Deletes the selected steps or connection. |
| `⌘` `Z` | Undoes. Add Shift to redo. |
| `⌘` `C` | Copies the selected steps. ⌘X cuts, ⌘V pastes and ⌘D duplicates. |
| `⌘` `A` | Selects every step. Shift and drag selects an area. |
| `+` | Zooms in. - zooms out, ⇧1 fits everything and ⇧0 goes to 100%. |
| `⌘` `↵` | Opens Run, or starts the run from the test input. |
| `Esc` | Closes the step settings or the add menu. |

## Accessibility

- The List view is the screen-reader alternative to the canvas. It’s an ordered list in run order, each step is a button naming its type and settings, branches are links to the step they lead to, and adding, running, approving and retrying all work from it.
- On the canvas every step and connection is focusable and labelled, such as “Classify email, Model step”, and handles and add buttons name the step they belong to.
- Changes are announced through a polite live region: added, deleted, connected, undone, a step waiting for approval, a failed step and how the run ended. Refused connections explain why, for example “That would run in circles”.
- Status is never colour alone. Every state has an icon and a word, a taken branch is also marked in text, and skipped connections turn dashed.
- Selection, hover and keyboard focus look different. A selected step gets a blue ring and a soft rainbow shadow underneath and reads “Selected” to screen readers, keyboard focus adds a separate ring around it, and the open step in the List view, the run log and History is marked as current. Opening a step in the run log selects it on the canvas and pans it into view.
- Motion respects reduced motion: the run flow, card pop-in, connection draw and arrange glide are replaced by instant changes, and the rainbow shadow under the final or selected step stays still.

## Built with

The free HextaUI components Workflow is made from. Each one installs on its own.

- [Button](https://hextaui.com/docs/button) - Buttons in every variant and size, with a built-in loading, success and error flow that skips the spinner for fast requests.
- [Button group](https://hextaui.com/docs/button-group) - Buttons joined into one control, with shared seams, separators, text addons, nesting and vertical stacks.
- [Collapsible](https://hextaui.com/docs/collapsible) - A panel that shows and hides with height motion you can reverse mid-way, without jumping the layout.
- [Command](https://hextaui.com/docs/command) - A searchable list of actions, inline or as a ⌘K palette, with pages, shortcuts and highlighted matches.
- [Dropdown menu](https://hextaui.com/docs/dropdown-menu) - A menu of actions and options behind a button, with groups, submenus, checkbox and radio items, and shortcuts.
- [Empty](https://hextaui.com/docs/empty) - A placeholder for screens with nothing to show yet, with an icon, a message and the next action.
- [Field](https://hextaui.com/docs/field) - Labels, descriptions and errors wired to their control, with validation states and layouts for forms.
- [Input](https://hextaui.com/docs/input) - A text input with three sizes, invalid and read-only states, native validation styling and a 16px touch font so phones never zoom in.
- [Kbd](https://hextaui.com/docs/kbd) - Key caps for shortcuts that show the right symbols on every platform, read them out properly and press down with the real keys.
- [Popover](https://hextaui.com/docs/popover) - A floating panel anchored to a trigger that resizes smoothly with its content and follows the trigger’s direction.
- [Select](https://hextaui.com/docs/select) - Pick one or more options from a list that opens on the current value, with typeahead, groups and form support.
- [Sheet](https://hextaui.com/docs/sheet) - A panel that slides in from any edge, with swipe-to-dismiss, scroll lock and stacked nesting.
- [Slider](https://hextaui.com/docs/slider) - Pick a value or a range by dragging, tapping the track or using the keys, with jumps that glide and an optional value bubble.
- [Spinner](https://hextaui.com/docs/spinner) - A loading indicator with Apple-style ticks or a breathing ring that can wait before showing and stay long enough not to flicker.
- [Tabs](https://hextaui.com/docs/tabs) - Switch between views with an indicator that slides to the active tab, as a segmented control or an underline, and lists that scroll when they overflow.
- [Textarea](https://hextaui.com/docs/textarea) - Multi-line text that grows smoothly with what you write between a minimum and maximum number of rows, with counters, validation and a submit shortcut.
- [Toast](https://hextaui.com/docs/toast) - Brief messages that stack neatly, expand on hover, swipe away, and turn a loading state into success or error in place.
- [Toggle](https://hextaui.com/docs/toggle) - A button that stays on or off, with a fill that settles in when pressed, a clear hover-to-on step and icons that can fill with the state.
- [Toggle group](https://hextaui.com/docs/toggle-group) - A row of toggles for one choice or many, with a fill that slides to the picked item, joined or spaced items and arrow-key focus.
- [Tooltip](https://hextaui.com/docs/tooltip) - A short hint on hover or keyboard focus that opens after a brief rest, switches instantly between neighbours and shows shortcuts.

## Code

14 files, added to components/blocks/workflow.
