Flow Builder
<zn-flow-builder> | ZnFlowBuilder
A drag-and-drop visual flow builder: steps panel, pan/zoom canvas, and a config inspector.
The Flow Builder is a three-panel editor for visual automations, with an optional header action bar:
-
Header (optional) — a full-width action bar rendered only when
slot="header-left"/slot="header-right"content is provided (e.g. Close / Undo All Changes on the left, Apply Changes on the right). - Left panel — the steps panel (searchable, tabbed by step group).
-
Canvas (center) — a pannable, zoomable surface. Connections are
append-only: click any output port on a node to start a
stray branch — the wire follows your cursor until you click a node (or its output port)
to attach it. Fan-in is allowed, and so are loops — a branch may point back to an earlier
step (e.g. an answer that restarts the questioning); only wiring a node directly to itself is refused.
Cancel by clicking empty canvas, pressing Esc, or leaving the window. A connected output port spawns a
brand-new branch on click, so extra arrows never require config forms. Open outputs draw nothing while
idle — their
+targets appear only while dragging a step in or moving one. Every existing wire has a+at its midpoint to insert a step between two nodes. New steps are created by dragging them from the panel onto the canvas or a+. Reposition nodes by dragging, and add resizable sticky notes. Scroll to pan (side-scroll or Shift+scroll pans horizontally); Ctrl/Cmd+scroll or trackpad pinch zooms toward the cursor. Toolbar: undo, redo, add-note, untangle (auto-arranges a messy flow into evenly spaced layers that follow the connections — undoable), zoom-in, reset, zoom-out. -
Right panel — the sidebar by default: a slot (
slot="sidebar") for status / version history, with a built-in Configuration Errors summary derived fromerrorNodes. Selecting a node slides that step’s configuration in from the right (driven by the node type’srenderConfig); clicking a branch pill on a wire slides in the branch editor instead — rename the branch and configure the filters/conditions for taking that path. Node types that declarebranchFilters(in markup or JS) get the built-in conditions editor — pick filters from a searchable list and combine them into AND groups joined by OR, saved onto the output port’sdata.conditions. A customrenderBranchConfigtakes precedence when supplied.
Both side panels can be tucked away with the chevrons on the canvas edges, giving the canvas the full width; selecting a node or branch brings the right panel back automatically.
A node’s … menu offers Delete, Duplicate, and Move — Move
detaches the step (with its subtree) so you can re-attach it to a different + slot.
Every node except an entrypoint (a node with inputs="") can have as many inputs and outputs as
it likes — outputs are per-node overridable, so a step’s config can add and remove branches at runtime.
It ships with no built-in node types — every step you see below is registered by the consumer. This keeps the component generic and lets it be extended for any future workflow without changing the library.
<div style="height: 900px;"> <zn-flow-builder id="flow-demo" heading="Retention – Annual Cancellation_v1" subheading="Last updated 16th June 2026" triggers-hint="Automations require a trigger to start. Choose a trigger below and drag it to the canvas to begin." actions-hint="Actions run as the automation progresses. Drag one onto the canvas or a + slot." rules-hint="Rules branch the flow based on conditions you configure on each step."> <zn-flow-step type="webhook" group="entrypoint" category="Events" icon="webhook@lu" color="rgb(236, 68, 91)" description="User hits webhook XYZ">Cancellation Requested </zn-flow-step> <zn-flow-step type="manual" group="entrypoint" category="Contacts" icon="user-plus@lu" color="rgb(236, 68, 91)">Contact added manually </zn-flow-step> <zn-flow-step type="list-add" group="entrypoint" category="Contacts" icon="list-plus@lu" color="rgb(236, 68, 91)">Contact added to a list </zn-flow-step> <zn-flow-step type="email-parser" group="action" category="Email" icon="mail@lu" color="rgb(43, 192, 145)">IMAP Email Parser </zn-flow-step> <zn-flow-step type="send-reply" group="action" category="Email" icon="send@lu" color="rgb(43, 192, 145)">Send Reply </zn-flow-step> <zn-flow-step type="claude" group="action" category="AI" icon="bot@lu" color="rgb(43, 192, 145)">Claude </zn-flow-step> <zn-flow-step type="notify" group="action" category="Support" icon="user@lu" color="rgb(43, 192, 145)">Notify User </zn-flow-step> <zn-flow-step type="split" group="rule" category="Logic" icon="split@lu" color="rgb(105, 54, 245)" outputs='[{"id":"true","label":"TRUE"},{"id":"false","label":"FALSE"}]'>Conditional Split </zn-flow-step> <div slot="sidebar" class="flow-sidebar-demo"> <label>Status <zn-select> <zn-option selected>Active</zn-option> <zn-option>Paused</zn-option> <zn-option>Draft</zn-option> </zn-select> </label> <h4>Version History</h4> <ul> <li><strong>John Smith</strong><span>16th June 2026 · 14:03</span></li> <li><strong>John Smith</strong><span>16th June 2026 · 11:20</span></li> <li><strong>Jane Doe</strong><span>15th June 2026 · 09:48</span></li> </ul> </div> </zn-flow-builder> </div> <style> .flow-sidebar-demo { margin-top: 16px; font-size: 0.8125rem; } .flow-sidebar-demo label { display: flex; flex-direction: column; gap: 4px; font-weight: 600; } .flow-sidebar-demo select { padding: 6px 8px; border: 1px solid rgb(var(--zn-border-color)); border-radius: 6px; } .flow-sidebar-demo h4 { margin: 16px 0 8px; } .flow-sidebar-demo ul { list-style: none; margin: 0; padding: 0; } .flow-sidebar-demo li { display: flex; flex-direction: column; padding: 8px 0; border-top: 1px solid rgb(var(--zn-border-color)); } .flow-sidebar-demo li span { color: rgb(var(--zn-color-muted-text)); font-size: 0.75rem; } </style> <script type="module"> const initial = { nodes: [ {id: 'n1', type: 'webhook', x: 280, y: 60, data: {subtitle: 'User hits webhook XYZ'}}, {id: 'n2', type: 'split', x: 280, y: 220, data: {subtitle: 'ARR Threshold'}}, {id: 'n3', type: 'email-parser', x: 80, y: 480, data: {subtitle: 'Import Email Attachment'}}, {id: 'n4', type: 'notify', x: 80, y: 640, data: {subtitle: 'Notify Frank Furt'}}, {id: 'n5', type: 'claude', x: 480, y: 480, data: {subtitle: 'Generate Reply'}}, {id: 'n6', type: 'send-reply', x: 480, y: 640, data: {subtitle: 'Generate Reply'}} ], connections: [ {id: 'c1', source: {node: 'n1', port: 'out'}, target: {node: 'n2', port: 'in'}}, {id: 'c2', source: {node: 'n2', port: 'true'}, target: {node: 'n3', port: 'in'}}, {id: 'c3', source: {node: 'n2', port: 'false'}, target: {node: 'n5', port: 'in'}}, {id: 'c4', source: {node: 'n3', port: 'out'}, target: {node: 'n4', port: 'in'}}, {id: 'c5', source: {node: 'n5', port: 'out'}, target: {node: 'n6', port: 'in'}} ], notes: [ {id: 'note1', x: 760, y: 440, text: 'This is a sticky note.'} ] }; // The Conditional Split's outputs are user-configurable per node: its renderConfig // edits node.outputs (add / rename / remove branches). The canvas re-renders the // outputs and the builder prunes connections to any removed branch. const splitType = { type: 'split', label: 'Conditional Split', group: 'rule', category: 'Logic', icon: 'split@lu', color: 'rgb(105, 54, 245)', outputs: [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}], // Clicking one of this step's branch pills opens the built-in conditions // editor, offering these filters (AND/OR groups saved on the port's data). branchFilters: [ { id: 'engagement', label: 'Email engagement', fields: [ { id: 'count', type: 'number', suffix: 'time(s)', value: 2, operators: [ {value: 'at-least', label: 'at least'}, {value: 'at-most', label: 'at most'}, {value: 'exactly', label: 'exactly'} ] }, { id: 'window', type: 'number', value: 6, operators: [ {value: 'in-last', label: 'in the last'}, {value: 'before-last', label: 'before the last'} ], units: [ {value: 'days', label: 'day(s)'}, {value: 'weeks', label: 'week(s)'}, {value: 'months', label: 'month(s)'} ] } ] }, { id: 'lost-reason', label: 'Lost reason', fields: [ { id: 'reason', operators: [ {value: 'eq', label: 'Is equal to'}, {value: 'neq', label: 'Is not equal to'} ], options: [ {value: 'price', label: 'Price'}, {value: 'competitor', label: 'Competitor'}, {value: 'no-need', label: 'No need'}, {value: 'other', label: 'Other'} ] } ] }, { id: 'arr', label: 'Annual recurring revenue', fields: [ { id: 'amount', type: 'number', suffix: 'USD', operators: [ {value: 'gte', label: 'is at least'}, {value: 'lte', label: 'is at most'} ] } ] } ], renderConfig: (node, update) => { const branches = node.outputs ?? [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}]; const setBranches = (next) => { node.outputs = next; update({}); }; const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;flex-direction:column;gap:8px'; // Branches are created on the canvas; the manager only shows once the // node has real ones — connected, or named and waiting to be attached. const el = document.getElementById('flow-demo'); const connected = new Set( el.getState().connections.filter(c => c.source.node === node.id).map(c => c.source.port)); if (!branches.some(b => b.label || connected.has(b.id))) return wrap; const heading = document.createElement('p'); heading.textContent = 'Branches'; heading.style.cssText = 'margin:0;font-weight:600;font-size:0.8125rem'; wrap.appendChild(heading); branches.forEach((branch, i) => { const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:6px;align-items:center'; const input = document.createElement('zn-input'); input.setAttribute('size', 'small'); input.value = branch.label ?? branch.id; input.style.flex = '1'; input.addEventListener('change', () => setBranches(branches.map((b, j) => (j === i ? {...b, label: input.value} : b)))); row.appendChild(input); const remove = document.createElement('zn-button'); remove.setAttribute('icon', 'x@lu'); remove.setAttribute('icon-button', 'small'); remove.setAttribute('plain', ''); remove.addEventListener('click', () => { const next = branches.filter((_, j) => j !== i); // Deleting the last branch leaves a plain open output (a fresh id, so // the removed branch's wire is pruned) — the node stays attachable. setBranches(next.length ? next : [{id: 'branch-' + Date.now().toString(36)}]); }); row.appendChild(remove); wrap.appendChild(row); }); const add = document.createElement('zn-button'); add.setAttribute('size', 'small'); add.setAttribute('icon', 'plus@lu'); add.textContent = 'Add branch'; add.addEventListener('click', () => setBranches([...branches, {id: 'branch-' + (branches.length + 1), label: 'New branch'}])); wrap.appendChild(add); return wrap; } }; customElements.whenDefined('zn-flow-builder').then(() => { const el = document.getElementById('flow-demo'); el.registerNodeType(splitType); el.setState(initial); el.errorNodes = ['n3']; }); </script>
Examples
Empty builder
With no node types registered, the steps panel is empty. Register types to populate it.
<div style="height: 560px;"> <zn-flow-builder heading="New Flow" subheading="Drafting"></zn-flow-builder> </div>
Declaring the steps panel
The builder owns the steps panel: Entrypoints / Triggers / Actions / Rules tabs with a
search box. You don’t author the tabs or their layout — you just declare the node types, and each is routed
to the right tab by its group and grouped under a collapsible category. A tab only
renders when its group has at least one registered step, so unused groups never appear.
Declare each type as a <zn-flow-step> (the items themselves are never shown — they’re
declarations that drive the rendered panel):
type(required) — the node type id.-
group—entrypoint|trigger|action|rule; picks the tab (defaults toaction). category— collapsible group within the tab (e.g. “Email”).- label — the slotted text (or a
labelattribute). icon,icon-library,color,description.-
inputs— a JSON array of input ports. Omit for a single default input; useinputs=""for a trigger with no input. -
outputs— a JSON array of ports, each a string id or a{"id","label"}object (e.g.outputs='[{"id":"true","label":"TRUE"},{"id":"false","label":"FALSE"}]'). Omit for a single default output.
Set an optional per-tab hint with the triggers-hint / actions-hint /
rules-hint attributes on the builder. (A node’s inspector renderConfig can’t be
expressed in markup — supply it via registerNodeTypes() for the same type id if
you need a custom config panel. Branch conditions can be expressed in markup — see the next
section.)
<zn-flow-builder triggers-hint="Choose a trigger and drag it to the canvas."> <zn-flow-step type="webhook" group="trigger" category="Events" icon="webhook@lu" color="rgb(236,68,91)" inputs="" description="User hits webhook XYZ">Cancellation Requested </zn-flow-step> <zn-flow-step type="split" group="rule" category="Logic" icon="split@lu" outputs='[{"id":"true","label":"TRUE"},{"id":"false","label":"FALSE"}]'>Conditional Split </zn-flow-step> </zn-flow-builder>
Branch conditions in markup
Clicking a branch pill opens the branch editor. For step types that declare branch filters, it renders the built-in conditions editor: a searchable filter picker, then the picked filters combined into AND groups joined by OR — with Save / Cancel, so edits only apply (undoably) on Save.
Declare the filters when instantiating the builder by nesting <zn-flow-filter> elements
inside a <zn-flow-step> (use the label attribute for the step’s name when
nesting). Each filter holds one <zn-flow-filter-field> per control row, and each field
declares its choices as nested <zn-flow-operator> /
<zn-flow-option> elements — the element text is the label, with an optional
value attribute for the stored value.
Try it: click the High intent or Everyone else pill to open the conditions editor.
<div style="height: 640px;"> <zn-flow-builder id="branch-filter-demo" heading="Win-back Campaign" subheading="Branch filters demo" rules-hint="Rules branch the flow based on conditions configured on each branch pill."> <zn-flow-step type="cancel-requested" group="entrypoint" category="Events" icon="webhook@lu" color="rgb(236, 68, 91)" inputs="">Cancellation Requested </zn-flow-step> <zn-flow-step type="split" group="rule" category="Logic" icon="split@lu" color="rgb(105, 54, 245)" label="Conditional Split" outputs='[{"id":"a","label":"Branch A"},{"id":"b","label":"Branch B"}]'> <zn-flow-filter id="engagement" label="Email engagement" description="How often the contact opened a campaign email"> <zn-flow-filter-field id="count" type="number" suffix="time(s)" value="2"> <zn-flow-operator value="gte">at least</zn-flow-operator> <zn-flow-operator value="lte">at most</zn-flow-operator> <zn-flow-operator value="eq">exactly</zn-flow-operator> </zn-flow-filter-field> <!-- Three adjustable controls: operator, amount, and time-frame unit. --> <zn-flow-filter-field id="window" type="number" value="6"> <zn-flow-operator value="within">in the last</zn-flow-operator> <zn-flow-operator value="before">before the last</zn-flow-operator> <zn-flow-unit value="days">day(s)</zn-flow-unit> <zn-flow-unit value="weeks">week(s)</zn-flow-unit> <zn-flow-unit value="months">month(s)</zn-flow-unit> </zn-flow-filter-field> </zn-flow-filter> <zn-flow-filter id="lost-reason" label="Lost reason" description="Why the customer said they cancelled"> <zn-flow-filter-field id="reason"> <zn-flow-operator value="eq">Is equal to</zn-flow-operator> <zn-flow-operator value="neq">Is not equal to</zn-flow-operator> <zn-flow-option value="price">Price</zn-flow-option> <zn-flow-option value="competitor">Competitor</zn-flow-option> <zn-flow-option value="no-need">No need</zn-flow-option> <zn-flow-option value="other">Other</zn-flow-option> </zn-flow-filter-field> </zn-flow-filter> <zn-flow-filter id="plan" label="Subscription plan"> <zn-flow-filter-field id="name"> <zn-flow-operator value="is">is</zn-flow-operator> <zn-flow-operator value="is-not">is not</zn-flow-operator> <zn-flow-option value="starter">Starter</zn-flow-option> <zn-flow-option value="growth">Growth</zn-flow-option> <zn-flow-option value="enterprise">Enterprise</zn-flow-option> </zn-flow-filter-field> </zn-flow-filter> <!-- Well-known operator keys don't need text — gte/lte get default labels. --> <zn-flow-filter id="arr" label="Annual recurring revenue"> <zn-flow-filter-field id="amount" type="number" suffix="USD" placeholder="0"> <zn-flow-operator value="gte"></zn-flow-operator> <zn-flow-operator value="lte"></zn-flow-operator> </zn-flow-filter-field> </zn-flow-filter> <zn-flow-filter id="last-order" label="Last order" description="Time since the contact last placed an order"> <zn-flow-filter-field id="days" type="number" suffix="day(s) ago" value="30"> <zn-flow-operator value="within">within the last</zn-flow-operator> <zn-flow-operator value="over">more than</zn-flow-operator> </zn-flow-filter-field> </zn-flow-filter> <zn-flow-filter id="tag" label="Contact tag"> <zn-flow-filter-field id="name" type="text" placeholder="e.g. vip"> <zn-flow-operator value="has">contains</zn-flow-operator> <zn-flow-operator value="not-has">does not contain</zn-flow-operator> </zn-flow-filter-field> </zn-flow-filter> </zn-flow-step> <zn-flow-step type="send-offer" group="action" category="Email" icon="mail@lu" color="rgb(43, 192, 145)">Send Win-back Offer </zn-flow-step> <zn-flow-step type="notify-csm" group="action" category="Support" icon="user@lu" color="rgb(43, 192, 145)">Notify Account Manager </zn-flow-step> </zn-flow-builder> </div> <script type="module"> customElements.whenDefined('zn-flow-builder').then(() => { const el = document.getElementById('branch-filter-demo'); el.setState({ nodes: [ {id: 'n1', type: 'cancel-requested', x: 300, y: 40, data: {}}, { id: 'n2', type: 'split', x: 300, y: 280, data: {subtitle: 'Qualify the save attempt'}, // The branch pills carry their saved conditions on the port's data. outputs: [ { id: 'a', label: 'High intent', data: { conditions: [ [ {filter: 'engagement', values: {count: {operator: 'gte', value: 2}, window: {operator: 'within', value: 6, unit: 'months'}}}, {filter: 'lost-reason', values: {reason: {operator: 'eq', value: 'price'}}} ], [ {filter: 'arr', values: {amount: {operator: 'gte', value: 5000}}} ] ] } }, {id: 'b', label: 'Everyone else'} ] }, {id: 'n3', type: 'send-offer', x: 100, y: 580, data: {subtitle: '20% off for 3 months'}}, {id: 'n4', type: 'notify-csm', x: 500, y: 580, data: {subtitle: 'Manual follow-up'}} ], connections: [ {id: 'c1', source: {node: 'n1', port: 'out'}, target: {node: 'n2', port: 'in'}}, {id: 'c2', source: {node: 'n2', port: 'a'}, target: {node: 'n3', port: 'in'}}, {id: 'c3', source: {node: 'n2', port: 'b'}, target: {node: 'n4', port: 'in'}} ], notes: [] }); }); </script>
<zn-flow-filter> attributes: id (required), label,
description (shown under the label in the picker).
<zn-flow-filter-field> attributes:
id(required) — key the field’s operator/value are stored under.-
type—select|number|text. Defaults toselectwhen options are given, elsetext. -
suffix— trailing unit text (e.g.time(s)) when the unit isn’t adjustable. -
label,placeholder,value(the initial value when the filter is added).
Inside a field, <zn-flow-operator> elements declare the operator dropdown (shown before
the value — “at least”, “is equal to”), <zn-flow-option> elements the value choices of a
select field, and <zn-flow-unit> elements an
adjustable unit dropdown after the value (day(s) / week(s) / month(s)) — so a row can have
three adjustable controls: operator, amount, and time frame. The element text is the display label; add
value="…" to store a stable key instead of the text. Empty operators with a
well-known key get a default label —
<zn-flow-operator value="gte"></zn-flow-operator> renders as “Greater Than or Equal
To”. Recognised keys: eq, neq, gt, gte, lt,
lte, is, is-not, in, not-in,
contains, not-contains, starts-with, ends-with,
empty, not-empty, within, before, between,
matches.
For programmatic generation the same lists also fit in operators / options
attributes (comma-separated values, or a JSON array of strings and/or
{"value","label"} objects), a whole catalog fits in a
branch-filters JSON attribute on the step, and JS registration takes a
branchFilters array on the FlowNodeType — as in the full demo at the top of this
page. A type supplying its own renderBranchConfig overrides the built-in editor.
Saved conditions live on the output port as port.data.conditions, an array of OR-ed groups,
each an array of AND-ed conditions:
[ [ // group 1 (AND-ed)… {filter: 'engagement', values: {count: {operator: 'gte', value: 2}, window: {operator: 'within', value: 6, unit: 'months'}}}, {filter: 'lost-reason', values: {reason: {operator: 'eq', value: 'price'}}} ], // …OR group 2 [{filter: 'arr', values: {amount: {operator: 'gte', value: 5000}}}] ]
Registering custom node types
Node types are the modular extension point. Instead of (or alongside) slotted steps, register an array of
FlowNodeType objects with registerNodeTypes() (or set the
.nodeTypes property) — the two sources merge into the same panel. Each type’s
group/category place it in the tabs; its icon, color, input/output ports, and
optional renderConfig define the node’s behavior and inspector.
import {html} from 'lit'; const builder = document.querySelector('zn-flow-builder'); builder.registerNodeTypes([ { type: 'send-email', label: 'Send Email', group: 'action', // 'trigger' | 'action' | 'rule' — which steps-panel tab category: 'Email', // collapsible group within the tab icon: 'mail@lu', color: 'rgb(43, 192, 145)', defaultData: {subject: ''}, // Inspector UI for a selected node of this type: renderConfig: (node, update) => html` <zn-input label="Subject" .value=${node.data.subject ?? ''} @zn-input=${e => update({subject: e.target.value})}></zn-input> `, }, { type: 'ab-split', label: 'A/B Split', group: 'rule', icon: 'split@lu', // Multiple outputs become labelled connection points (TRUE / FALSE here): outputs: [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}], }, ]);
A node type with inputs: [] is a starting point (a trigger) with no incoming port. A type with
multiple outputs renders one labeled port per output, so a branch can fan out to different
steps.
User-configurable outputs
A type’s outputs are the default. For steps whose branches are defined by the user (e.g. a
conditional split), set node.outputs on the instance from
renderConfig — the canvas renders those ports, and the builder prunes any connection whose
branch is removed. node.inputs works the same way. Set the new ports then call
update({}) to commit:
renderConfig: (node, update) => { const branches = node.outputs ?? [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}]; const addBranch = () => { node.outputs = [...branches, {id: crypto.randomUUID(), label: 'New branch'}]; update({}); // commit — re-renders the outputs, prunes dropped branches' connections }; // …render an editor for `branches` that calls addBranch / removes / renames… }
Reacting to changes
The builder emits events as the flow is edited. Use zn-flow-change to persist, or read
.value (a JSON string of the full FlowState) at any time.
const builder = document.querySelector('zn-flow-builder'); builder.addEventListener('zn-flow-change', e => { console.log('flow updated', e.detail.state); }); builder.addEventListener('zn-flow-selection-change', e => { console.log('selected node', e.detail.nodeId); }); builder.addEventListener('zn-flow-connect', e => { console.log('new connection', e.detail.connection); }); // Persist / restore — `value` is the full FlowState, node positions included: localStorage.setItem('flow', builder.value); builder.value = localStorage.getItem('flow'); // Saving to a server: the builder serialises directly (toJSON returns the // FlowState), so the POST body is one expression either way: await fetch('/api/flows/123', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(builder), // === builder.value });
Auto-save
Add the auto-save attribute to periodically snapshot the flow (positions included) into
localStorage, keyed by the builder’s id (falling back to its heading), with a
1-day TTL:
<zn-flow-builder id="my-flow" auto-save></zn-flow-builder> <!-- every 5 minutes --> <zn-flow-builder id="my-flow" auto-save="2"></zn-flow-builder> <!-- every 2 minutes -->
Without the attribute nothing is saved. An empty canvas is never written (so a fresh session can’t clobber a stored flow), and expired snapshots are purged when encountered.
While auto-save is on, a status pill in the canvas’s bottom-left flashes “Auto-saved” as
each snapshot lands and otherwise shows how long ago the last one happened (“Last saved 3m ago”). And when a
flow is loaded (setState / value) that
differs from a fresh auto-saved draft, a banner offers to restore the draft — editing the
loaded flow or dismissing the banner keeps what was loaded. Restoring can also be done programmatically:
const builder = document.querySelector('zn-flow-builder'); if (builder.restoreAutoSave()) { console.log('Restored an auto-saved draft'); // false when none exists / it expired }
Importing
If you’re using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use any of the following snippets to cherry pick this component.
To import this component from the CDN using a script tag:
<script type="module" src="https://cdn.jsdelivr.net/npm/@kubex/zinc@1.1.28/dist/components/flow-builder/flow-builder.js"></script>
To import this component from the CDN using a JavaScript import:
import 'https://cdn.jsdelivr.net/npm/@kubex/zinc@1.1.28/dist/components/flow-builder/flow-builder.js';
To import this component using a bundler:
import '@kubex/zinc/dist/components/flow-builder/flow-builder.js';
Slots
| Name | Description |
|---|---|
| (default) |
<zn-flow-step> type declarations; never displayed, each group/category
routes the step into the right tab and collapsible grouping of the rendered panel. A step may nest
<zn-flow-filter> declarations (each holding
<zn-flow-filter-field>s, whose operator / option choices are nested
<zn-flow-operator> / <zn-flow-option> elements) — or set a
branch-filters JSON attribute — to drive the built-in branch conditions editor.
|
header-left
|
Actions shown on the left of the header bar (e.g. Close / Undo All Changes). |
header-right
|
Actions shown on the right of the header bar (e.g. Apply Changes). |
sidebar
|
Extra right-panel content (status, version history), below the configuration errors. |
Learn more about using slots.
Properties
| Name | Description | Reflects | Type | Default |
|---|---|---|---|---|
nodeTypes
|
Node types to make available, registered into the internal registry. |
FlowNodeType[]
|
[]
|
|
errorNodes
|
Node ids flagged as having configuration errors (drives the red node styling). |
string[]
|
[]
|
|
autoSave
auto-save
|
Auto-save the flow to localStorage (1-day TTL). Omit to disable. A bare
auto-save saves every 5 minutes; a numeric value sets the interval in minutes
(auto-save="5"). Restore with restoreAutoSave().
|
number | null
|
null
|
|
entrypointsHint
entrypoints-hint
|
Optional hint shown beneath each steps-panel tab. |
string
|
''
|
|
untangle
|
Auto-arrange the nodes into evenly spaced layers that follow the flow, animating them into place (wires and pills track them since everything is derived from the node coordinates). Undoable as a single step. | - | - | |
updateComplete |
A read-only promise that resolves when the component has finished updating. |
Learn more about attributes and properties.
Events
| Name | React Event | Description | Event Detail |
|---|---|---|---|
zn-flow-change |
|
Emitted whenever the flow state changes. event.detail.state is the new FlowState.
|
- |
zn-flow-selection-change |
|
Emitted when the selected node changes. event.detail.nodeId. |
- |
zn-flow-connect |
|
Emitted when a connection is created. event.detail.connection. |
- |
Learn more about events.
Methods
| Name | Description | Arguments |
|---|---|---|
restoreAutoSave() |
Load the auto-saved flow, if one exists within the 1-day TTL. | - |
toJSON() |
The full flow state — nodes with their positions, connections, branch data, and notes — ready for
persisting. Lets JSON.stringify(builder) serialize the flow directly, e.g. as a POST
body.
|
- |
Learn more about methods.
Parts
| Name | Description |
|---|---|
base |
The grid wrapper. |
header |
The full-width header action bar (only rendered when header slots are filled). |
steps |
The left steps panel. |
inspector |
The right panel while a node or branch is selected. |
Learn more about customizing CSS parts.
Dependencies
This component automatically imports the following dependencies.
<zn-dropdown><zn-example><zn-flow-branch-conditions><zn-flow-canvas><zn-flow-node><zn-icon><zn-input><zn-navbar><zn-tabs><zn-tooltip>