npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flowget/builder

v0.3.1

Published

Composable React workflow builder for Flowget — Provider + compound subcomponents, token-driven theming, replaceable primitives, slot-based customization, and position-optional graph loading (autoLayout / mergeGraph via dagre) for code-generated and AI-au

Downloads

259

Readme

@flowget/builder

Composable React workflow builder for Flowget — Provider + compound subcomponents, token-driven theming, replaceable primitives, slot-based customization.

Alpha (0.1.0-alpha.0). See CHANGELOG.md for phase-by-phase history of what's shipped. The README below documents the currently shipped surface only — no future-promised features mixed in.

Full documentation

The README is the quick-reference. Long-form docs live under docs/:

Install

npm install @flowget/builder @xyflow/react react react-dom
# or: bun add / pnpm add / yarn add — package is PM-agnostic

Peer dependencies (you bring them):

  • react ^18.3 || ^19
  • react-dom ^18.3 || ^19
  • @xyflow/react ^12.0v11 is not supported. The builder is built on @xyflow/react@12; v11 has a different node/edge data shape and connection API.

Internal state is powered by zustand and bundled into the published artifact — you do not need to install zustand yourself. The bundled instance has private module identity, so your app's zustand stores stay isolated from the builder's.

Quick start

// app/workflows/[id]/page.tsx
import { FlowgetBuilder } from "@flowget/builder";
import "@flowget/builder/styles.css";
import { storage, execution, catalog } from "./adapters";

export default function Page({ params }: { params: { id: string } }) {
  return (
    <FlowgetBuilder
      workflowId={params.id}
      storage={storage}
      execution={execution}
      catalog={catalog}
    />
  );
}

The default <FlowgetBuilder> composes Provider + Toolbar + ActiveRunBanner + Catalog + Canvas + Inspector + HitlSlot.

Adapter optionality

Only storage is required. Omit any of the others to switch off the corresponding surface:

// Embeddable read-only past-run viewer — no execute, no edit, schema-evolution-resistant
<FlowgetBuilder workflowId={pastRunId} storage={storage} />

| Adapter | Optionality | Effect when omitted | |---|---|---| | storage | required | (can't render without a workflow to load) | | execution | optional | Toolbar drops Run / Cancel / status pill; ActiveRunBanner becomes inert | | catalog | optional | Unknown node types render via a raw-shape fallback (type id + data snapshot, no field-aware UI). Catalog sidebar shows a "no catalog provided" state. Lets you render past workflows whose node types have evolved since save. | | hitl | optional | HITL surfaces silently absent; HITL signals (if any) are no-ops |

Conditional adapters under exactOptionalPropertyTypes: true (the recommended TS setting) require the conditional-spread shape:

<FlowgetBuilder
  storage={storage}
  {...(includeExecution ? { execution } : {})}
  {...(includeCatalog ? { catalog } : {})}
/>

Passing execution={undefined} is rejected under strict optional-property semantics. Either omit the prop entirely or use the spread pattern above.

Compose your own layout when the default doesn't fit:

<FlowgetBuilder.Provider storage={storage} execution={execution} catalog={catalog}>
  <FlowgetBuilder.Toolbar />
  <main style={{ display: "flex", flex: 1 }}>
    <FlowgetBuilder.Catalog />
    <FlowgetBuilder.Canvas />
    <FlowgetBuilder.Inspector />
  </main>
  <FlowgetBuilder.HitlSlot strategy="drawer" />
</FlowgetBuilder.Provider>

Compose your own Toolbar

The Toolbar accepts children; sub-primitives (SaveAction, RunAction, DebugAction, etc.) consume internal hooks for their state, so customer composition needs zero context plumbing:

<FlowgetBuilder.Toolbar>
  <FlowgetBuilder.Toolbar.Breadcrumb />
  <div className="fg-toolbar__actions">
    <FlowgetBuilder.Toolbar.SaveAction />
    <MyCustomButton />
    <FlowgetBuilder.Toolbar.DebugAction />
    <FlowgetBuilder.Toolbar.RunAction />
    <FlowgetBuilder.Toolbar.StatusPill />
  </div>
</FlowgetBuilder.Toolbar>

Default behavior (no children) renders the standard cluster. The same pattern applies to Catalog, Inspector, Canvas, and HitlSlot. Item-mapping renderers (e.g. <ValidationOverlay renderIssue={...} /> and the nodeRenderers / edgeRenderers / fieldRenderers registries) stay render-prop / registry — that's the architect-locked seam from CAR-447 §C.3.

Reading builder state from your components

import {
  useFlowgetBuilder,        // mega-hook — all slices at once
  useFlowgetSelection,      // { selectedNodeIds, primary, setSelection, addToSelection, ... }
  useFlowgetWorkflow,       // { graph, isDirty, addNode, updateNodeData, moveNode, ... }
  useFlowgetExecution,      // { status, events, beginRun, ... } | null  ← null if no ExecutionAdapter
  useFlowgetValidation,     // { issues, blockingErrors, warnings, setIssues }
  useFlowgetNodeStatus,     // per-node status derived from execution events (memoized)
  useFlowgetTheme,
  useFlowgetPrimitives,
} from "@flowget/builder";

The canonical Q1 use case — "row click in side panel → select corresponding node in canvas":

const { setSelection } = useFlowgetSelection();
// ...
<button onClick={() => setSelection([targetNodeId])}>Inspect</button>

useFlowgetNodeStatus(nodeId) returns the node's current run status ("idle" | "running" | "succeeded" | "failed" | "cancelled" | "skipped") derived from the execution event stream. Memoized on (nodeId, events) so the O(events) scan only runs when events arrive, not per render.

Per-node context menu (ui.menu)

Catalog authors declare per-node menu items via BuilderHints.ui.menu. Right-clicking the node opens the menu; selection dispatches the Provider's onNodeMenuAction:

// In the catalog
const myNode: NodeMetadata<BuilderHints> = {
  id: "http_request",
  category: "action",
  handles: [/* ... */],
  label: "HTTP Request",
  ui: {
    menu: [
      { type: "item", id: "copy_curl", label: "Copy as cURL" },
      { type: "item", id: "test_endpoint", label: "Send test request", disabledIfRunning: true },
      { type: "separator", id: "sep1" },
      { type: "item", id: "view_logs", label: "View recent logs" },
    ],
  },
};

// In the page
<FlowgetBuilder
  storage={storage}
  catalog={catalog}
  onNodeMenuAction={(itemId, nodeId, ctx) => {
    if (itemId === "copy_curl") copyCurlForNode(ctx);
    if (itemId === "test_endpoint") fireSingleNodeTest(ctx.nodeId);
    // ...
  }}
/>

NodeMenuContext carries { workflowId, nodeId, nodeType, data } — enough for routing decisions without re-querying the store.

Writing a custom node renderer

The nodeRenderers registry maps a node type to a component that renders that node on the canvas. Customer components accept React Flow's NodeProps directly — Flowget-specific data (catalog metadata, status, validation) reaches them via hooks:

import { type NodeProps } from "@xyflow/react";
import {
  useFlowgetNodeStatus,
  NodeHandles,
} from "@flowget/builder";

type SlackData = { channel?: string };

function SlackMessageCard({ id, type, data, selected }: NodeProps<SlackData>) {
  const status = useFlowgetNodeStatus(id);
  return (
    <>
      <NodeHandles typeId={type ?? ""} />
      <div className={`slack-card slack-card--${status}`} data-selected={selected}>
        <h3>Slack message</h3>
        <p>Channel: {data.channel ?? "(unset)"}</p>
      </div>
    </>
  );
}

<FlowgetBuilder
  storage={storage}
  catalog={catalog}
  nodeRenderers={{ slack_message: SlackMessageCard }}
/>

<NodeHandles typeId={...} /> reads the catalog's HandleSpec[] for the node type and renders the right RF <Handle> elements with the right positions + ids. Use it to avoid hand-positioning Handle elements (which can drift away from the catalog's declared shape). If you prefer to wire Handles yourself, import { Handle, Position } from "@xyflow/react" and render them directly — same pattern, different ergonomics.

Same approach for edgeRenderers (customer accepts RF's EdgeProps, uses BaseEdge / getBezierPath / etc. from @xyflow/react).

HITL — human-in-the-loop signals

Customer pushes a HITL signal onto the queue (typically from an adapter integration when an execution event indicates a node is awaiting human input):

import { useFlowgetHitl } from "@flowget/builder";

function MyHitlTrigger() {
  const hitl = useFlowgetHitl();
  return (
    <button onClick={() => hitl.pushSignal({
      key: "approve",
      nodeId: "n5",
      nodeType: "approval_step",
      request: { reason: "User exceeded daily limit" },
    })}>
      Simulate signal
    </button>
  );
}

<HitlSlot /> reads the first pending signal and delivers it to the customer's HitlSlot component (registered via <Provider hitl={MyHitlSlot}>). Four rendering strategies:

  • strategy="modal" (default) — wraps in the Dialog primitive
  • strategy="drawer" — wraps in a side panel
  • strategy="inline" — renders in-place, customer handles positioning
  • strategy="off" — skips the slot; customer mounts <HitlSlot.Renderer /> elsewhere in their tree

Customer's HitlSlot component receives { open, onClose, signal, onResolve } and decides the UI (form, chat, approval buttons, etc).

Toolbar dialogs

Default <Toolbar /> auto-mounts <SaveWorkflowDialog />, <DebugDialog />, <RunTestDialog /> as siblings of the toolbar row. They wire to existing store state (saveDialogOpen / debugDialogOpen / runTestDialogOpen) and use the customer-replaceable Dialog primitive. Customer composing their own Toolbar children takes responsibility for mounting their own (or omitting them):

<FlowgetBuilder.Toolbar>
  <FlowgetBuilder.Toolbar.Breadcrumb />
  <FlowgetBuilder.Toolbar.SaveAction />
  <MyCustomButton />
  <FlowgetBuilder.Toolbar.RunAction />
  {/* Mount the dialogs you want */}
  <FlowgetBuilder.Toolbar.SaveWorkflowDialog />
  <FlowgetBuilder.Toolbar.RunTestDialog />
  {/* Omit DebugDialog if not needed */}
</FlowgetBuilder.Toolbar>

Composing your own Inspector

The Inspector is a Hybrid C area-slot — empty children renders Header + Body + ExpandFieldDialog; pass children for full layout control:

<FlowgetBuilder.Inspector>
  <MyCustomHeader />
  <FlowgetBuilder.Inspector.Body />
  <FlowgetBuilder.Inspector.VariablesPanel />
  <MyCustomDocsLink />
  <FlowgetBuilder.Inspector.ExpandFieldDialog />
</FlowgetBuilder.Inspector>

For per-field control, render <Inspector.Field field={...} /> directly:

<FlowgetBuilder.Inspector>
  <FlowgetBuilder.Inspector.Header />
  <div className="my-grid">
    <FlowgetBuilder.Inspector.Field field={urlFieldDecl} />
    <FlowgetBuilder.Inspector.Field field={methodFieldDecl} />
  </div>
</FlowgetBuilder.Inspector>

For maximum control, use the hooks directly — your component manages everything:

import { useFlowgetInspector, useFlowgetField } from "@flowget/builder";

function UrlField() {
  const { value, setValue, field, issues } = useFlowgetField<string>("url");
  if (field === undefined) return null;
  return (
    <label>
      {field.label ?? "URL"}
      <input
        value={value ?? ""}
        onChange={(e) => setValue(e.target.value)}
        aria-invalid={issues.some(i => i.severity === "error")}
      />
    </label>
  );
}

Writing a custom field renderer

The fieldRenderers registry maps a FieldType (or custom:${string} escape hatch) to a component. Customer components accept FieldRendererProps<TValue> and render against your design system:

import type { FieldRendererProps } from "@flowget/builder";

function MyText({ field, value, onChange, disabled, issues }: FieldRendererProps<string>) {
  return (
    <div className="my-field">
      <label>{field.label ?? field.key}</label>
      <input
        value={value}
        onChange={(e) => onChange(e.target.value)}
        disabled={disabled}
      />
      {issues?.map(i => <p key={i.id} className={`alert alert-${i.severity}`}>{i.message}</p>)}
    </div>
  );
}

<FlowgetBuilder
  storage={storage}
  catalog={catalog}
  fieldRenderers={{
    text: MyText,
    "custom:colorpicker": MyColorPicker,
  }}
/>

Precedence chain (resolved internally by pickFieldRenderer):

  1. fieldRenderers["custom:xyz"] if the catalog declares customType: "xyz"
  2. defaultFieldRenderers["custom:xyz"] (rare — customer-provided defaults registered globally)
  3. fieldRenderers[field.type]
  4. defaultFieldRenderers[field.type]
  5. UnknownFieldFallback — renders raw JSON + "no renderer registered" message

Wrap-not-replace shape: customer imports the default and composes:

import { TextField, type FieldRendererProps } from "@flowget/builder";

function MyWrappedText(props: FieldRendererProps<string>) {
  return (
    <div className="my-field-wrapper">
      <TextField {...props} />
    </div>
  );
}

onInsertVariable is @experimental in the current version. Customer renderers receive onInsertVariable(token) on FieldRendererProps, but the focus-routing wiring that delivers VariablesPanel clicks to the right field lands in a future iteration. Calling onInsertVariable from a renderer today is a no-op; the contract shape is stable, only the wiring is missing. See the JSDoc on FieldRendererProps.onInsertVariable for the focused-field model planned next.

Composing your own catalog sidebar

The Catalog is a Hybrid C area-slot — empty children renders the default body (Search + grouped List); pass children to take full layout control:

<FlowgetBuilder.Catalog defaultCollapsed={false}>
  <MyCustomBranding />
  <FlowgetBuilder.Catalog.Search placeholder="Search nodes..." />
  <MyCustomDivider />
  <FlowgetBuilder.Catalog.List
    grouping="category"
    itemRenderer={(node) => (
      <li className="my-card" data-category={node.category}>
        <strong>{node.label ?? node.id}</strong>
        {node.description ? <span>{node.description}</span> : null}
      </li>
    )}
    emptyState={<p>Bring your own empty-state copy here.</p>}
    noResultsState={<p>Nothing matches your query.</p>}
  />
</FlowgetBuilder.Catalog>
  • grouping="category" (default) groups items by NodeCategory. Set to "none" for a flat list.
  • itemRenderer is the render-prop override per item — Hybrid C item-mapping pattern. Customer's renderer is fully responsible for the <li> (or whatever element) shape, including drag-out handling if needed (text/flowget-node-type payload).
  • defaultCollapsed initializes uncontrolled collapse state. Customer who needs persistence wraps <Catalog> and forwards collapsed through their own state machine.
  • hideCollapseToggle hides the built-in toggle button when customer drives collapse externally.

Default item shape (<Catalog.Item />) is composable too — drop the itemRenderer and the default item renderer handles drag-out + collapsed-mode automatically.

Switching workflows: remount the Provider

<FlowgetBuilder.Provider> holds the Zustand store across re-renders, but workflowId / initialGraph are baked at first mount — they don't re-initialize on prop change. To switch between workflows inside the same parent tree, give the Provider a React key:

<FlowgetBuilder
  key={currentWorkflowId}            // remounts the whole builder + store on workflow change
  workflowId={currentWorkflowId}
  storage={storage}
  catalog={catalog}
/>

Mid-mount adapter / handler swaps are picked up via React context — the store stays intact. Only the workflow seed and the per-mount selection / execution state need a full remount.

CSS

import "@flowget/builder/styles.css";   // complete visual contract (recommended)
// — or à la carte (partial — see note below) —
import "@flowget/builder/tokens.css";   // only design tokens
import "@flowget/builder/reset.css";    // only the scoped reset

styles.css is the complete visual contract — design tokens + scoped reset + builder component styles + the @xyflow/react base layer (pane cursor, edge geometry, viewport transforms, zoom/pan). It is self-contained: you do not need to import @xyflow/react/dist/style.css separately (doing so anyway is harmless — the rules are idempotent). The à-la-carte tokens.css / reset.css are partial — they do not carry the builder component styles or the React Flow base, so use them only if you're assembling the full sheet yourself.

Themes are token overrides. The builder ships a neutral default plus data-theme="flowget" (brand) and data-theme="light" (light variant) opt-ins. Define your own by setting data-theme="<name>" and shipping a CSS rule:

[data-theme="acme"] {
  --flowget-color-accent: #ff6b35;
  --flowget-color-bg: #0b0e14;
  /* …override any of the ~45 documented tokens — see src/styles/tokens.css for the full list */
}
<FlowgetBuilder theme="acme" />

All tokens live under the --flowget-* namespace and are scoped to .fg-builder-root. The reset is similarly scoped — it will not fight your app's global CSS, Tailwind preflight, or your design system.

Extension authors — @flowget/builder/internal

For third-party @flowget/<ext>-style packages, not end-users. If you're embedding the builder in your app, you never import this.

The builder's React contexts + accessor hooks (useBuilderStoreApi, adapters, theme, primitives, renderers, callbacks) are published at the bare specifier @flowget/builder/internal. Every entry of this package — including the Provider and the useFlowget* hooks — imports them from there, so the createContext calls live in one shipped module that a bundler resolves once.

If you publish a component package that renders inside a customer's <FlowgetBuilder.Provider> (a custom field renderer, node renderer, inspector panel, etc.) and needs builder state, import from @flowget/builder/internal and mark both @flowget/builder and @flowget/builder/internal as peerDependencies + bundler external — exactly how you'd externalize react. That guarantees your extension reads the same Provider state the host writes, instead of bundling its own (dead) context instance.

// your @flowget/my-extension package.json
"peerDependencies": {
  "@flowget/builder": "^0.1.0",
  "react": "^18 || ^19"
}
// your extension component
import { useBuilderStoreApi } from "@flowget/builder/internal";

Architecture

Compound-component pattern (Radix-style). Provider injects adapters + theme + primitives + renderers; named subcomponents read from the React context tree and from the per-mount Zustand store.

Per architect verdict on CAR-447 §C.3 (Hybrid C):

  • Area slots — Toolbar / Catalog / Canvas / Inspector / HitlSlot — accept children. Sub-primitives use hooks for state. No render-prop ceremony, no (ctx) => plumbing.
  • Item-mapping slotsrenderIssue on ValidationOverlay; nodeRenderers / edgeRenderers / fieldRenderers registries — kept as render-prop / registry (iteration boundary makes children syntactically awkward there).
  • PrimitivesDialog, Tooltip, Popover, ContextMenu, Toast — replaced wholesale via <Provider primitives={{ Dialog: MyDialog }}>. Defaults are minimal HTML + ARIA; bring your own Radix / Headless UI / custom set if you need real focus traps / positioning. Primitives are headless: Flowget does not inject classes or styles on them. Customer-supplied primitives carry their own styling — what you ship is exactly what renders. The default primitives use the .fg-dialog-* / .fg-tooltip / .fg-popover etc. classes you can target from your token overrides, but replacement primitives are on their own.

The full architecture spec is CAR-447.

Developing locally

bun install                # or npm / pnpm / yarn
bun run build              # tsup → dist/{index.js,index.cjs,index.d.{ts,cts}} + dist/*.css
bun run typecheck
bun run lint
bun run test

License

FSL-1.1-ALv2. See LICENSE.