# React Workflow Builder: Triggers, Branches and AI Steps

> Build a React workflow builder on React Flow: a step catalog, yes / no branches, AI steps, validation and live run status, plus a rule list for simple flows.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 16, 2026
- Category: [Build it: screens and apps](https://wingo-ui.com/blog/category/build)
- Reading time: 13 min
- Canonical: https://wingo-ui.com/blog/react-workflow-builder

## TL;DR

A React workflow builder is React Flow plus the layers it leaves to you: a step catalog, a settings panel per step, validation (one trigger, no loops, no dangling branches), undo and a run view that shows each step's status. The Wingo UI Workflow Canvas ships those layers on @xyflow/react and stores the automation as plain { nodes, edges } JSON that your own server runs. For one-trigger automations, the Rule Builder edits the same idea as a When / If / Then list that reads like a sentence.

You want your users to set up their own automations: when an invoice goes overdue, check the balance, have an AI draft a reminder, send it. The obvious start is React Flow, and an afternoon later you have boxes and lines on a canvas. Then the real work shows up. A React workflow builder needs a catalog of steps, a settings form per step, branches that cannot loop, undo, a way to show a run, and a layout someone can use on a phone. This tutorial builds that editor on React Flow with triggers, yes / no branches and AI steps, then builds a similar automation as a list-style rule builder for people who would rather not drag lines. The examples use [Workflow Canvas](https://wingo-ui.com/components/workflow-canvas) and [Rule Builder](https://wingo-ui.com/components/rule-builder) from Wingo UI, the library we build, so we are not neutral. The data model and the checks apply to any implementation.

## What does a React workflow builder need on top of React Flow?

It needs seven layers of application code, because React Flow (`@xyflow/react`) only gives you the canvas: nodes, edges, handles, pan, zoom and a minimap. This is the list we worked from:

| Layer | What it does | In the Workflow Canvas |
| --- | --- | --- |
| Step catalog | Which triggers, actions, AI steps and logic exist | `nodeTypes`, grouped in the add menu |
| Branches | A condition splits the flow | Yes and No outputs, stored as `sourceHandle` |
| Settings panel | Edit the selected step | `renderInspector`, a panel on desktop and a sheet on phones |
| Validation | Stop broken flows from running | `validateWorkflow`, red rings on the steps |
| History | Undo a bad drag or delete | Undo and redo, one step per committed change |
| Run mode | Show what the automation is doing | `running` plus `data.status` per step |
| Layout | Keep the graph readable | `layoutWorkflow` and the Tidy button |

The value is plain JSON, `{ nodes, edges }`, so you can save it, diff it and run it on the server with whatever engine you already have. The canvas never executes anything.

## How do I install the workflow canvas?

Install it with the Wingo UI CLI, which copies the source into your project:

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

The CLI writes `components/ui/workflow-canvas.tsx` with its Wingo UI dependencies (button, textarea, drawer, alert dialog and others) and installs `@xyflow/react`, `motion` and `lucide-react`. The component imports the React Flow base CSS itself. It fills its parent, so give the parent a height. The settings form later in this post also uses a select, which is not one of those dependencies: run `npx wingo-ui@latest add select` too.

## How do I define triggers, actions and AI steps?

Each step type is a `WorkflowNodeDef` with a `kind`: `trigger`, `action`, `condition`, `ai`, `delay` or `end`. The kind sets the icon tile and the rules (a trigger takes no incoming line, a condition gets two outputs). Keep the catalog at module scope so it keeps its identity between renders.

```tsx
// app/automations/node-types.tsx
import { Flag, Hourglass, Mail, Receipt, Sparkles, Split } from "lucide-react"
import type { WorkflowNodeDef } from "@/components/ui/workflow-canvas"

export const nodeTypes: WorkflowNodeDef[] = [
  {
    type: "invoice-overdue",
    kind: "trigger",
    label: "Overdue invoice",
    description: "When an invoice passes its due date",
    icon: <Receipt />,
    group: "Triggers",
    defaults: { subtitle: "After 30 days" },
  },
  {
    type: "condition",
    kind: "condition",
    label: "Condition",
    description: "Splits the flow into Yes and No",
    icon: <Split />,
    group: "Logic",
  },
  {
    type: "ai-draft",
    kind: "ai",
    label: "Draft with AI",
    description: "Writes the message from your instructions",
    icon: <Sparkles />,
    group: "AI",
    defaults: { subtitle: "Polite tone", config: { prompt: "Write a short, polite payment reminder." } },
    keywords: ["llm", "write", "gpt", "claude"],
  },
  {
    type: "send-email",
    kind: "action",
    label: "Send email",
    icon: <Mail />,
    group: "Communication",
  },
  { type: "delay", kind: "delay", label: "Wait", icon: <Hourglass />, group: "Logic" },
  { type: "end", kind: "end", label: "End", icon: <Flag />, group: "Logic" },
]
```

`group` becomes a heading in the add menu, and `keywords` let someone find the AI step by typing "llm". A condition gets Yes and No outputs unless you pass your own `outputs`, such as three tiers for a lead score.

## How do I build a branch with yes and no outputs?

A branch is two edges from the same condition, each with a `sourceHandle` that names the output (`yes` or `no`). Steps can start at `0, 0`, and `layoutWorkflow` places them in layers from the trigger, with the branches side by side in the order of the condition's outputs. The canvas would also lay out an all-zero graph once on mount, but calling `layoutWorkflow` yourself means the value you save already holds real positions.

```tsx
// app/automations/initial.ts
// imported by client components only: workflow-canvas.tsx is a "use client" module
import { layoutWorkflow, type WorkflowEdge, type WorkflowNode, type WorkflowValue } from "@/components/ui/workflow-canvas"
import { nodeTypes } from "./node-types"

const step = (id: string, type: string, title: string): WorkflowNode => ({
  id,
  type,
  position: { x: 0, y: 0 },
  data: { title },
})

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

export const initial: WorkflowValue = {
  nodes: layoutWorkflow(
    [
      step("start", "invoice-overdue", "Invoice 30 days overdue"),
      step("check", "condition", "Balance over $1,000?"),
      step("draft", "ai-draft", "Draft the reminder"),
      step("send", "send-email", "Email the client"),
      step("stop", "end", "Nothing to send"),
    ],
    edges,
    { nodeTypes },
  ),
  edges,
}
```

Render it in a client component. `AlertDialogProvider` goes once near the root, because deleting a connected step asks for confirmation through it:

```tsx
// app/automations/overdue-editor.tsx (first version)
"use client"

import { useState } from "react"
import { AlertDialogProvider } from "@/components/ui/alert-dialog"
import { WorkflowCanvas } from "@/components/ui/workflow-canvas"
import { initial } from "./initial"
import { nodeTypes } from "./node-types"

export function OverdueEditor() {
  const [workflow, setWorkflow] = useState(initial)
  return (
    <div className="h-[560px]">
      <WorkflowCanvas nodeTypes={nodeTypes} value={workflow} onValueChange={setWorkflow} />
      <AlertDialogProvider />
    </div>
  )
}
```

`onValueChange` fires once per committed change: a drop, a connection, an inserted step, an undo. A drag does not flood your state with positions, and a value that arrives mid-drag waits until the drop. The canvas also refuses bad connections while you drag: a line into a trigger, a line that would close a loop, or a second line from the same branch turns red and does not land.

> Live demo (Workflow Canvas): Drag from the dot under a step to another step, then try to connect back into the trigger and watch the line turn red. Press Run to see each step go from queued to running to done. Try it at [Workflow Canvas](https://wingo-ui.com/components/workflow-canvas) and install it with `npx wingo-ui@latest add workflow-canvas`.

Hover a line for its + button to insert a step in between, or drop a line on empty space to open the add menu there. Tidy rearranges the graph, and the move is one undo step.

## How do I give each step its own settings?

Pass a `renderInspector` function: it returns the settings form for the selected step, which opens in a panel on the right. You get the step and an `api` with `update` (merges into the step's data), `def` (the step's catalog entry), `fields` (the default name, description and skip fields) and `readOnly`. Here the AI step gets a prompt and a tone, and the other steps keep the default fields:

```tsx
// app/automations/inspect.tsx
import type { ReactNode } from "react"
import { Select } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import type { WorkflowInspectorApi, WorkflowNode } from "@/components/ui/workflow-canvas"

const TONES = [
  { value: "polite", label: "Polite" },
  { value: "firm", label: "Firm" },
]

export function inspect(node: WorkflowNode, { update, fields, readOnly }: WorkflowInspectorApi): ReactNode {
  if (node.type !== "ai-draft") return fields
  const config = node.data.config ?? {}
  return (
    <>
      {fields}
      <Textarea
        label="Instructions for the AI"
        value={String(config.prompt ?? "")}
        onValueChange={(prompt) => update({ config: { ...config, prompt } })}
        readOnly={readOnly}
        minRows={3}
      />
      <Select
        label="Tone"
        options={TONES}
        value={String(config.tone ?? "polite")}
        onValueChange={(tone) =>
          update({ config: { ...config, tone }, subtitle: `${TONES.find((t) => t.value === tone)?.label} tone` })
        }
        readOnly={readOnly}
      />
    </>
  )
}
```

`update` merges shallowly into `data`, so spread the old `config` or you lose the other settings. Typing in the prompt counts as one undo step per 500ms burst, so Ctrl+Z does not erase a paragraph one letter at a time. If your app already has a side panel, set `inspector="none"` and open your own from `onSelectedIdChange`.

## How do I validate a workflow before saving?

Call `validateWorkflow(value, nodeTypes)`: it returns the problems that stop an automation from running as `{ nodeId?, edgeId?, message }` objects. It catches no trigger or more than one, a connection into a trigger, a loop, a condition branch that goes nowhere, an unknown step type and a connection to a missing step. The canvas already rings those steps in red. On save, select the first broken step so its settings open, and drop the run state before the graph leaves the browser:

```ts
// app/automations/save.ts
import { validateWorkflow, type WorkflowValue } from "@/components/ui/workflow-canvas"
import { nodeTypes } from "./node-types"

// status and error describe the last run, not the automation
const withoutRunState = (value: WorkflowValue): WorkflowValue => ({
  ...value,
  nodes: value.nodes.map(({ data: { status: _status, error: _error, ...data }, ...node }) => ({ ...node, data })),
})

export async function saveWorkflow(workflow: WorkflowValue, select: (id: string | null) => void) {
  const issues = validateWorkflow(workflow, nodeTypes)
  if (issues.length > 0) {
    select(issues.find((issue) => issue.nodeId)?.nodeId ?? null)
    return false
  }
  const response = await fetch("/api/automations/overdue", {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(withoutRunState(workflow)),
  })
  return response.ok
}
```

Run the same checks on the server, because the client can be bypassed. One Next.js trap: `workflow-canvas.tsx` starts with `"use client"`, so a route handler that imports `validateWorkflow` from it gets a client reference, and calling it throws "Attempted to call validateWorkflow() from the server". Copy the checks you need into a server-only module instead.

## How do I show a run on the canvas?

Set `running` and write each step's `data.status`; the canvas draws the rest. `onRun` adds a Run button to the toolbar, `running` locks editing, and the status shows as a marker. The status type is the same one the [AI agent tool call UI](https://wingo-ui.com/blog/ai-agent-tool-call-ui) uses: `pending`, `running`, `complete` and `error`, plus `canceled` and `awaiting-approval` for an AI step that waits for a person to approve its output. The line into the running step flows, finished lines turn green, and the view follows the running step unless the person panned in the last 3 seconds. Your server runs the steps and streams their status:

```ts
// app/automations/run.ts
import type { Dispatch, SetStateAction } from "react"
import type { WorkflowNodeData, WorkflowValue } from "@/components/ui/workflow-canvas"

type StepEvent = { stepId: string; status: WorkflowNodeData["status"]; error?: string }

export async function startRun(
  automationId: string,
  setWorkflow: Dispatch<SetStateAction<WorkflowValue>>,
  setRunning: (running: boolean) => void,
) {
  setRunning(true)
  const response = await fetch(`/api/automations/${automationId}/runs`, { method: "POST" })
  if (!response.ok) return setRunning(false)
  const { runId } = await response.json()
  const source = new EventSource(`/api/runs/${runId}/events`)
  const stop = () => {
    source.close()
    setRunning(false)
  }
  source.onmessage = (event) => {
    const { stepId, status, error } = JSON.parse(event.data) as StepEvent
    setWorkflow((current) => ({
      ...current,
      nodes: current.nodes.map((node) =>
        node.id === stepId ? { ...node, data: { ...node.data, status, error } } : node,
      ),
    }))
  }
  source.addEventListener("done", stop)
  source.onerror = stop
}
```

A failed step shows its `error` in the inspector. AI steps fail in the most varied ways (a timeout, a refusal, a rate limit), so send the model's own error text so the person can tell them apart.

## How do I wire save, settings and runs into one editor?

Hold the workflow, the selection and the run flag in the editor, and hand each helper what it needs. This replaces the first version of `overdue-editor.tsx`:

```tsx
// app/automations/overdue-editor.tsx
"use client"

import { useState } from "react"
import { AlertDialogProvider } from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { WorkflowCanvas } from "@/components/ui/workflow-canvas"
import { initial } from "./initial"
import { inspect } from "./inspect"
import { nodeTypes } from "./node-types"
import { startRun } from "./run"
import { saveWorkflow } from "./save"

export function OverdueEditor() {
  const [workflow, setWorkflow] = useState(initial)
  const [selectedId, setSelectedId] = useState<string | null>(null)
  const [running, setRunning] = useState(false)

  return (
    <div className="flex flex-col gap-3">
      <Button className="self-end" disabled={running} onClick={() => saveWorkflow(workflow, setSelectedId)}>
        Save
      </Button>
      <div className="h-[560px]">
        <WorkflowCanvas
          nodeTypes={nodeTypes}
          value={workflow}
          onValueChange={setWorkflow}
          selectedId={selectedId}
          onSelectedIdChange={setSelectedId}
          renderInspector={inspect}
          onRun={() => startRun("overdue", setWorkflow, setRunning)}
          running={running}
        />
      </div>
      <AlertDialogProvider />
    </div>
  )
}
```

## What is a simpler alternative to a node-based workflow editor?

A rule list is the simpler alternative: a When, If, Then stack of cards that reads like a sentence. Most automations are one trigger, a few conditions and a few actions, and a list says that more plainly than a graph. The Rule Builder adds a summary box that reads the whole rule back as one sentence.

```bash
npx wingo-ui@latest add rule-builder
```

```tsx
"use client"

import { useState } from "react"
import { Mail, Sparkles, UserPlus } from "lucide-react"
import {
  RuleBuilder,
  createRule,
  describeRule,
  type RuleActionDef,
  type RuleTriggerDef,
} from "@/components/ui/rule-builder"

const triggers: RuleTriggerDef[] = [
  {
    type: "lead-created",
    label: "New lead",
    icon: <UserPlus />,
    fields: [
      { id: "budget", label: "Budget", type: "currency" },
      {
        id: "source",
        label: "Source",
        type: "select",
        options: [
          { value: "referral", label: "Referral" },
          { value: "website", label: "Website" },
        ],
      },
    ],
    summary: () => "a **new lead** comes in",
  },
]

const actions: RuleActionDef[] = [
  {
    type: "ai-draft",
    label: "Draft a reply with AI",
    icon: <Sparkles />,
    tone: "info",
    params: [{ id: "prompt", label: "Instructions", type: "textarea", required: true }],
    summary: () => "draft a reply with **AI**",
    multiple: false,
  },
  {
    type: "send-email",
    label: "Send email",
    icon: <Mail />,
    params: [{ id: "to", label: "To", type: "text", required: true }],
    summary: (params) => `email **${params.to}**`,
  },
]

export function LeadRule() {
  const [rule, setRule] = useState(() => createRule())
  return (
    <RuleBuilder
      title="Reply to new leads"
      triggers={triggers}
      actionTypes={actions}
      value={rule}
      onValueChange={(next) => {
        setRule(next)
        console.log(describeRule(next, { triggers, actions }))
      }}
      onTest={async () => ({ matched: true, message: "Emma Carter's lead would get a drafted reply." })}
    />
  )
}
```

`multiple: false` takes the AI step out of the action picker once the rule uses it. `describeRule` returns the same sentence as the summary box, which is handy for notification text and the automation list. The conditions are a `lib/filters` group, the same model the [Filter Builder](https://wingo-ui.com/components/filter-builder) edits, so one evaluator can serve both. Apply saves an edit; Cancel or Escape throws the draft away, so `onValueChange` never sees an unapplied draft.

> Live demo (Rule Builder): Pick a card to edit it, add a condition, then drag an action by its grip to reorder. On a phone each editor opens as a bottom sheet. Press Test to see each action's outcome under the sentence. Try it at [Rule Builder](https://wingo-ui.com/components/rule-builder) and install it with `npx wingo-ui@latest add rule-builder`.

## Canvas or rule list: which one should I ship?

Ship the rule list when automations are linear and the canvas when they branch. Either way the React workflow builder UI only edits data, so the deciding questions are who builds the automations and how often one needs a second path.

| Your automations | Ship |
| --- | --- |
| One trigger, a few filters, a few actions | Rule Builder |
| Branches that lead to different steps, waits, AI steps mid-flow | Workflow Canvas |
| Both kinds of users | Rule Builder by default, canvas as the advanced view |

The two do not share a value shape (`RuleValue` versus `{ nodes, edges }`), so pick one per automation type instead of switching an existing automation between them.

## How do I add an automations list, run history and logs?

Use the [Automation Builder Page](https://wingo-ui.com/components/automation-builder-page), a block with the rest of the screen a React automation builder needs around the editor. It has a list of automations with on and off switches and a bar of each one's last 14 runs, the Workflow Canvas as the editor, a [cron picker](https://wingo-ui.com/components/cron-picker) for scheduled triggers, Run a test, and a History tab whose log uses the same viewer as our [React log viewer guide](https://wingo-ui.com/blog/react-log-viewer). Without `automations` it shows its sample data, so pass your own list and wire the callbacks to your API:

```tsx
"use client"

import { useState } from "react"
import { AutomationBuilderPage, type Automation, type AutomationRun } from "@/components/blocks/automation-builder-page"
import { nodeTypes } from "./node-types"

export function AutomationsScreen({ initial, runs }: { initial: Automation[]; runs: AutomationRun[] }) {
  const [automations, setAutomations] = useState(initial)
  return (
    <AutomationBuilderPage
      height="screen"
      nodeTypes={nodeTypes}
      automations={automations}
      onAutomationsChange={setAutomations}
      runs={runs}
      onToggle={async (automation, enabled) => {
        const response = await fetch(`/api/automations/${automation.id}`, {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ enabled }),
        })
        if (!response.ok) throw new Error("Could not update the automation")
      }}
      onSave={async (automation) => {
        const response = await fetch(`/api/automations/${automation.id}`, {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(automation),
        })
        if (!response.ok) throw new Error("Could not save the automation")
      }}
      onTest={async (automation) => {
        const response = await fetch(`/api/automations/${automation.id}/test`, { method: "POST" })
        if (!response.ok) throw new Error(await response.text())
      }}
    />
  )
}
```

Pass `nodeTypes` whenever your workflows use your own step catalog. Without it the block validates against its built-in CRM catalog, so every custom step counts as an unknown type: Save shows the problems instead of calling `onSave`, and switching the automation on is refused. The switch is optimistic: a rejected `onToggle` flips it back with a toast. A rejected `onSave` keeps the changes and shows a toast, which is why the handler throws on a failed response. A rejected `onTest` shows its message and opens the run's log. Inside an admin shell, use `height="fill"`; our [Next.js admin dashboard tutorial](https://wingo-ui.com/blog/nextjs-admin-dashboard-tutorial) shows that shell.

## How do I make a workflow builder work on a phone?

Make every touch gesture mean one thing. On the canvas one finger pans and two fingers pinch. A step has to be tapped (selected) before it drags, so panning across a card never moves it. Long-press opens a step's menu, handles reach 44px out from the card, and the settings open in a sheet that stops at half height so the canvas stays visible above it. The Rule Builder opens every editor and picker as a bottom sheet below 768px, and only the grip drags, so the rest of a card scrolls the page. That grip pattern is the one from our [dnd kit touch drag guide](https://wingo-ui.com/blog/dnd-kit-mobile-touch-drag).

Keyboard users get the same reach: Tab moves through the toolbar, every step and every connection; arrows move the selected step; A adds a step after it; Delete removes it.

## Should I build it myself or use a workflow builder SDK?

Build a React Flow workflow builder yourself if you want to own every layer and have the time; buy one when the editor is not your product. Facts as of October 2026, from each vendor's page:

| Option | License and price | Good fit |
| --- | --- | --- |
| [React Flow](https://github.com/xyflow/xyflow) with your own layers | MIT, free. The [Workflow Editor template](https://reactflow.dev/ui/templates/workflow-editor) is a Pro example; [React Flow Pro](https://reactflow.dev/pro) starts at $169 per month | You want to own every layer and have the time |
| [Workflow Builder SDK](https://www.workflowbuilder.io/pricing) by Synergy Codes | Community Edition Apache 2.0, free; Enterprise EUR 6,990 one time per organization | A JSON Schema properties panel, a reference backend and a Temporal adapter |
| Wingo UI Workflow Canvas and Rule Builder | [Wingo UI Pro](https://wingo-ui.com/pricing): $8 per month, $80 per year or $150 once | A canvas and a list editor in the same design system, phone layouts included |

If you want an xyflow example to study line by line, [React Flow's own examples](https://reactflow.dev/examples) are the best source. If your team already runs Temporal, the Workflow Builder SDK has the closer backend story.

## What should I build next?

Install the canvas and wire `onValueChange` to your own save endpoint: `npx wingo-ui@latest add workflow-canvas`. More screens built this way live in the [Build it category](https://wingo-ui.com/blog/category/build).

## Components in this post

- [Workflow Canvas](https://wingo-ui.com/components/workflow-canvas) (Pro): A React workflow builder: a node editor for automations with a trigger, actions, AI steps and yes / no branches you connect and watch run. Install: `npx wingo-ui@latest add workflow-canvas`
- [Rule Builder](https://wingo-ui.com/components/rule-builder) (Pro): A React rule builder for if-this-then-that automations: a trigger, conditions and ordered actions as sentence cards, read back as one sentence. Install: `npx wingo-ui@latest add rule-builder`
- [Automation Builder Page](https://wingo-ui.com/components/automation-builder-page) (Pro): A React automation builder page: rules with on and off switches and run health, a visual workflow editor, a cron inspector, tests and logs. Install: `npx wingo-ui@latest add automation-builder-page`

## FAQ

### Is React Flow free for building a workflow editor?

Yes. React Flow (@xyflow/react) is MIT licensed and free for commercial use. As of October 2026 its official Workflow Editor template is marked as a Pro example, and React Flow Pro starts at $169 per month on the Starter plan.

### How do I add conditional branches in React Flow?

Give the condition node two source handles, such as yes and no, and store the handle id on each edge as sourceHandle. Your runner then follows only the edges whose sourceHandle matches the result of the condition.

### How should I store a workflow built in React?

Store the graph as JSON: a nodes array with id, type, position and a data object for each step's settings, and an edges array with source, target and sourceHandle. Strip run state such as status and error before saving, and validate the graph again on the server.

### Can I call validateWorkflow in a Next.js route handler?

Not from the component file. workflow-canvas.tsx starts with "use client", so on the server Next.js replaces its exports with client references and calling one throws. Keep your server checks in a server-only module.

### Should I build a node canvas or a rule list for automations?

Use a rule list when most automations are one trigger, a few conditions and a few actions, because it reads as a sentence and works well on a phone. Use a canvas when people need branches that lead to different steps, waits and AI steps in the middle of a flow.

### Are the Wingo UI workflow components free?

No. The Workflow Canvas, the Rule Builder and the Automation Builder Page are part of Wingo UI Pro, which costs $8 per month, $80 per year or $150 once as of October 2026. They install as source code with npx wingo-ui@latest add workflow-canvas, rule-builder or automation-builder-page.

---

Source: https://wingo-ui.com/blog/react-workflow-builder
