Skip to content

React Component

Embed Workflow Builder in any React app.

  • React 18 or 19
  • @xyflow/react 12 or higher
  • ESM-compatible bundler (Vite, Webpack 5, Next.js, Parcel)
  • One instance per page (required). Multi-instance is not supported. Plugin / JsonForms / i18n registries are module-level singletons shared across mounts, and the imperative useStore.{getState,setState,subscribe} facade resolves through a module-level “current” pointer — two <WorkflowBuilder.Root> on the same page would silently fight over both. If you need multiple “workflows” on one page, render them sequentially (mount → save → unmount → mount next).

Install the SDK along with its peer dependencies:

Terminal window
npm install @workflowbuilder/sdk react react-dom @xyflow/react @jsonforms/core @jsonforms/react i18next react-i18next i18next-browser-languagedetector immer zustand

The SDK ships its non-peer dependencies bundled inside dist/, so the peer list above is everything you need to install yourself. React, xyflow, JsonForms, i18next, immer and zustand are kept external because they expose singletons (store identity, i18next instance, frozen-object caches) — your app and the SDK must share a single copy of each.

Installing from a local checkout (contributors)

Section titled “Installing from a local checkout (contributors)”

If you’re developing against an unpublished build of the SDK, run pnpm build:lib from the monorepo root to produce packages/sdk/dist/, then install the local path in your consumer:

Terminal window
npm install /path/to/workflow-builder/packages/sdk react react-dom @xyflow/react

Local-path installs can resolve React from the library’s own node_modules, breaking hook calls. Deduplicate it in your bundler — for Vite:

vite.config.ts
export default defineConfig({
resolve: { dedupe: ['react', 'react-dom', '@xyflow/react'] },
});

Published-to-npm installs don’t need this.

The SDK exposes a single compound component, WorkflowBuilder. Mount <WorkflowBuilder.Root> at the top of your editor subtree; with no children it renders the default layout (top bar, palette, canvas, properties panel). Compose with the named subcomponents when you need a custom layout.

import { WorkflowBuilder } from '@workflowbuilder/sdk';
import '@workflowbuilder/sdk/style.css';
function App() {
return (
<WorkflowBuilder.Root
name="my-workflow"
layoutDirection="DOWN"
nodeTypes={
[
/* PaletteItemOrGroup[] */
]
}
integration={{
strategy: 'props',
onDataSave: async (data) => {
console.log('Saving:', data);
return 'success';
},
}}
/>
);
}

Pass children to skip the default layout and compose your own:

<WorkflowBuilder.Root nodeTypes={myNodeTypes}>
<header>
<WorkflowBuilder.TopBar />
</header>
<aside>
<WorkflowBuilder.Palette />
</aside>
<main>
<WorkflowBuilder.Canvas />
</main>
<aside>
<WorkflowBuilder.PropertiesPanel />
</aside>
</WorkflowBuilder.Root>

To add custom overlays alongside the default layout, mount it explicitly:

<WorkflowBuilder.Root nodeTypes={myNodeTypes}>
<WorkflowBuilder.DefaultLayout />
<MyToastBanner />
</WorkflowBuilder.Root>
StrategySource / sinkWhen to use
localStorageBrowser localStoragePrototyping, quick starts
apiGET/POST to user-provided endpointsBackend-managed persistence
propsonDataSave callback + initialNodes/initialEdges propsHost-app-managed persistence
<WorkflowBuilder.Root integration={{ strategy: 'api', endpoints: { load: '/api/load', save: '/api/save' } }} />
PropTypeDescription
nodeTypesPaletteItemOrGroup[]Node type definitions. Appear in the palette and drive validation.
templatesTemplateModel[]Diagram templates available in the template selector.
integrationWorkflowBuilderIntegrationData source/sink. Defaults to localStorage.
jsonFormWorkflowBuilderJsonFormConfigCustom JsonForms renderers, cells, translations.
pluginsWorkflowBuilderPlugin[]Functions registering decorators. Synchronous, executed once.
namestringWorkflow name shown in the header.
layoutDirection'DOWN' | 'RIGHT'Initial flow direction.
initialNodesWorkflowBuilderNode[]Starting diagram nodes.
initialEdgesWorkflowBuilderEdge[]Starting diagram edges.

Node properties are rendered with JsonForms. Register custom renderers and cells via the jsonForm prop. Custom renderers are tried before built-ins so your tester can override matches on a tie.

import { rankWith, uiTypeIs } from '@jsonforms/core';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { WorkflowBuilder } from '@workflowbuilder/sdk';
import '@workflowbuilder/sdk/style.css';
function ColorPicker({
data,
handleChange,
path,
}: {
data: string;
handleChange: (path: string, value: string) => void;
path: string;
}) {
return <input type="color" value={data ?? '#000000'} onChange={(e) => handleChange(path, e.target.value)} />;
}
function App() {
return (
<WorkflowBuilder.Root
jsonForm={{
renderers: [
{
tester: rankWith(5, uiTypeIs('ColorPicker')),
renderer: withJsonFormsControlProps(ColorPicker),
},
],
}}
/>
);
}

Plugins are functions that register decorators. Pass them to the plugins prop:

import { WorkflowBuilder, type WorkflowBuilderPlugin, registerComponentDecorator } from '@workflowbuilder/sdk';
import { MyCustomButton } from './my-custom-button';
const myPlugin: WorkflowBuilderPlugin = () => {
registerComponentDecorator('OptionalAppBarControls', {
content: MyCustomButton,
name: 'MyPlugin',
});
};
function App() {
return <WorkflowBuilder.Root plugins={[myPlugin]} />;
}

Plugins are synchronous. If a plugin needs async work (config fetch, WASM load, feature flag lookup), the consumer awaits it outside the SDK and constructs the plugin around the resolved value before passing it to <WorkflowBuilder.Root>.

Available slots: OptionalAppBarControls, OptionalAppBarTools, OptionalAppChildren, OptionalEdgeProperties, OptionalFooterContent, OptionalHooks (invisible provider slot), OptionalNodeContent (receives nodeId prop).

Decorator options:

registerComponentDecorator('SlotName', {
content: MyComponent, // React component to render
place: 'before', // 'before' | 'after' | 'wrapper'
modifyProps: (p) => p, // modify the host component's props
priority: 0, // higher = rendered first
name: 'UniquePluginName', // prevents duplicate registration
});

You can also intercept functions and register translations:

import { registerFunctionDecorator, registerPluginTranslation } from '@workflowbuilder/sdk';
registerFunctionDecorator('trackFutureChange', {
place: 'before',
callback: ({ params }) => {
/* inspect */
},
});
registerPluginTranslation({
en: { translation: { plugins: { myPlugin: { label: 'My Plugin' } } } },
});

All public types are exported from @workflowbuilder/sdk. The full API Reference is generated by TypeDoc directly from the SDK source on every docs build, so it never drifts.