Skip to content

Build a plugin

Compose registerComponentDecorator, registerFunctionDecorator, and registerPluginTranslation into a plugin function passed to WorkflowBuilder.Root.

A plugin is a synchronous function that registers component decorators, function decorators, JsonForms extensions, and / or translations by calling the SDK’s register* APIs. <WorkflowBuilder.Root> invokes every plugin in the plugins prop in order, once on first mount, via a lazy useState initializer.

Combined with Custom JsonForms control, the three registration functions below cover most customisation needs.

All three are side-effecting and safe to call more than once — pass a name to deduplicate.

type WorkflowBuilderPlugin = () => void;

Most plugins combine multiple registrations. Wrap them in a function and pass via the plugins prop on <WorkflowBuilder.Root>:

const myPlugin: WorkflowBuilderPlugin = () => {
registerComponentDecorator('OptionalAppBarControls', {
content: MyButton,
name: 'my-plugin',
});
registerFunctionDecorator('trackFutureChange', {
place: 'after',
callback: ({ params }) => auditLog(params),
name: 'my-plugin',
});
};
<WorkflowBuilder.Root plugins={[myPlugin]} />;

Plugins can also be called directly — the registries are currently module-global singletons. See Known limitations.

Add, wrap, or modify a component mounted in a named slot.

function registerComponentDecorator<P = object>(slotName: string, options: ComponentDecoratorOptions<P>): void;
type ComponentDecoratorOptions<P = object> =
| {
place?: 'before' | 'after' | 'wrapper';
content: React.ElementType;
modifyProps?: (props: P) => P;
priority?: number;
name?: string;
}
| {
modifyProps?: (props: P) => P;
priority?: number;
name?: string;
};
  • place — where to render relative to the slot’s host:
    • 'before' (default) — render your content before the host.
    • 'after' — render after.
    • 'wrapper' — wrap the host entirely (your content receives the host as children).
  • content — the React component to render.
  • modifyProps — function receiving the host’s props, returning modified props.
  • priority — higher = rendered first. Default 0.
  • name — unique identifier within the slot; prevents duplicate registration across calls.
Slot nameWhere it renders
OptionalAppBarControlsApp bar — control buttons area
OptionalAppBarToolsApp bar — toolbar area
OptionalAppChildrenApp-level children (portals, providers)
OptionalEdgePropertiesEdge properties panel
OptionalFooterContentFooter area
OptionalHooksInvisible provider/hook slot
OptionalNodeContentInside nodes (receives nodeId prop)
import { registerComponentDecorator } from '@workflowbuilder/sdk';
import { MyCustomButton } from './my-custom-button';
registerComponentDecorator('OptionalAppBarControls', {
content: MyCustomButton,
name: 'MyPlugin',
priority: 10, // shown before decorators with lower priority
});

Pass the slot’s props type as the type parameter so modifyProps is checked against the actual host’s prop shape. Slots that target a built-in component export a matching *Props type from the SDK barrel — for example, DiagramContainerProps for the 'DiagramContainer' slot, ProjectSelectionProps for 'ProjectSelection', and PropertiesBarProps for 'PropertiesBar'.

import { registerComponentDecorator } from '@workflowbuilder/sdk';
import type { DiagramContainerProps } from '@workflowbuilder/sdk';
import { myEdgeTypes } from './edges';
registerComponentDecorator<DiagramContainerProps>('DiagramContainer', {
modifyProps: (props) => ({
...props,
edgeTypes: { ...props.edgeTypes, ...myEdgeTypes }, // typed against EdgeTypes
}),
});

For a custom slot you control, type the parameter with your component’s own props instead — registerComponentDecorator<MyButtonProps>('MyCustomSlot', { … }).

Intercept a decorable function before/after its execution.

function registerFunctionDecorator(functionName: string, options: FunctionDecoratorOptions): void;
type FunctionDecoratorOptions =
| { place?: 'before'; callback: CallbackBefore; priority?: number; name?: string }
| { place: 'after'; callback: CallbackAfter; priority?: number; name?: string };
type CallbackBefore = (args: { params: unknown[] }) => void | { replacedParams: unknown[] };
type CallbackAfter = (args: { params: unknown[]; returnValue: unknown }) => void | { replacedReturn: unknown };

A non-exhaustive list (grep withOptionalFunctionPlugins in the source for the complete set):

Function nameWhat it does
getPaletteDataBuilds the palette data structure.
getTemplatesBuilds the template list.
trackFutureChangeRecords an upcoming diagram change.
getControlsDotsItemsBuilds the dots-menu items in the app bar.
  • Before-decorator: return nothing (observe) or { replacedParams: [...] } (substitute arguments).
  • After-decorator: return nothing (observe) or { replacedReturn: ... } (substitute the result).
import { registerFunctionDecorator } from '@workflowbuilder/sdk';
// Run code BEFORE a function executes
registerFunctionDecorator('trackFutureChange', {
place: 'before',
callback: ({ params }) => {
console.log('Change incoming:', params);
},
});
// Run code AFTER and optionally replace the return value
registerFunctionDecorator('trackFutureChange', {
place: 'after',
callback: ({ params, returnValue }) => {
return { replacedReturn: modifiedValue };
},
});

Merge additional i18n resources into the plugins.* namespace.

function registerPluginTranslation(resource: PluginTranslationResource): void;
import { registerPluginTranslation } from '@workflowbuilder/sdk';
registerPluginTranslation({
en: {
translation: {
plugins: {
myPlugin: {
label: 'My Plugin',
description: 'Does something useful',
},
},
},
},
});

Equivalent to passing translations via <WorkflowBuilder.Root jsonForm={{ translations }} />. Use whichever is more convenient for your plugin’s lifecycle.