docs/docs/en/plugin-development/client/flow-engine/index.md
In NocoBase, the FlowEngine is the core engine that drives visual configuration. Blocks, fields, and action buttons in the NocoBase interface are all managed by FlowEngine — including their rendering, configuration panels, and configuration persistence.
For plugin developers, FlowEngine provides two core concepts:
If your component needs to appear in the "Add block / field / action" menu, or needs to support visual configuration by users in the interface, you need to wrap it with a FlowModel. If these capabilities aren't needed, a plain React component is sufficient — see Component vs FlowModel.
Unlike regular React components, FlowModel not only handles UI rendering but also manages where props come from, configuration panel definitions, and configuration persistence. In short: a regular component's props are hardcoded, while FlowModel's props are dynamically generated through Flows.
To learn more about FlowEngine's overall architecture, see FlowEngine Full Documentation. Below we introduce how to use it from a plugin developer's perspective.
Creating and registering a FlowModel takes three steps:
// models/HelloBlockModel.tsx
import React from 'react';
import { BlockModel } from '@nocobase/client-v2';
import { tExpr } from '@nocobase/flow-engine';
export class HelloBlockModel extends BlockModel {
renderComponent() {
return (
<div>
<h3>Hello FlowEngine!</h3>
<p>This is a custom block.</p>
</div>
);
}
}
// define() sets the display name in the menu
HelloBlockModel.define({
label: tExpr('Hello block'),
});
renderComponent() is the model's rendering method, similar to a React component's render(). tExpr() is used for deferred translation — because define() executes at module load time, when i18n hasn't been initialized yet. See Context Common Capabilities -> tExpr for details.
// plugin.tsx
import { Plugin } from '@nocobase/client-v2';
export class MyPlugin extends Plugin {
async load() {
this.flowEngine.registerModelLoaders({
HelloBlockModel: {
// Lazy loading — the module is loaded only when first used
loader: () => import('./models/HelloBlockModel'),
},
});
}
}
After registration and enabling the plugin (see Plugin Development Overview for enabling plugins), create a new page in the NocoBase interface and click "Add block" to see your "Hello block".
Rendering alone isn't enough — the core value of FlowModel lies in being configurable. Using registerFlow(), you can add configuration panels to a model, allowing users to modify properties in the interface.
For example, a block that supports editing HTML content:
// models/SimpleBlockModel.tsx
import React from 'react';
import { BlockModel } from '@nocobase/client-v2';
import { tExpr } from '@nocobase/flow-engine';
export class SimpleBlockModel extends BlockModel {
renderComponent() {
// this.props values come from the Flow handler
return <div dangerouslySetInnerHTML={{ __html: this.props.html }} />;
}
}
SimpleBlockModel.define({
label: tExpr('Simple block'),
});
SimpleBlockModel.registerFlow({
key: 'flow1',
title: tExpr('Simple Block Flow'),
on: 'beforeRender', // Execute before rendering
steps: {
editHtml: {
title: tExpr('Edit HTML Content'),
// uiSchema defines the configuration panel UI
uiSchema: {
html: {
type: 'string',
title: tExpr('HTML Content'),
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
},
// Default values
defaultParams: {
html: `<h3>This is a simple block</h3>
<p>You can edit the HTML content.</p>`,
},
// The handler sets the configuration panel values to the model's props
handler(ctx, params) {
ctx.model.props.html = params.html;
},
},
},
});
Key points here:
on: 'beforeRender' — Indicates this Flow executes before rendering; configuration panel values are written to this.props before renderinguiSchema — Defines the configuration panel UI in JSON Schema format. See UI Schema for syntax referencehandler(ctx, params) — params contains the values the user entered in the configuration panel, which are set to the model via ctx.model.propsdefaultParams — Default values for the configuration paneluiSchema is based on JSON Schema. v2 is compatible with uiSchema syntax, though its use cases are limited — it's mainly used in Flow configuration panels to describe form UI. For most runtime component rendering, it's recommended to use Ant Design components directly without going through uiSchema.
Here are the most commonly used component types (see UI Schema for a complete reference):
uiSchema: {
// Text input
title: {
type: 'string',
title: 'Title',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
// Multi-line text
content: {
type: 'string',
title: 'Content',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
// Dropdown select
type: {
type: 'string',
title: 'Type',
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: [
{ label: 'Primary', value: 'primary' },
{ label: 'Default', value: 'default' },
{ label: 'Dashed', value: 'dashed' },
],
},
// Switch
bordered: {
type: 'boolean',
title: 'Show border',
'x-decorator': 'FormItem',
'x-component': 'Switch',
},
}
Each field is wrapped with 'x-decorator': 'FormItem', which automatically adds a title and layout.
FlowModel.define() sets the model's metadata and controls how it appears in menus. The most commonly used parameter in plugin development is label, but it supports more:
| Parameter | Type | Description |
|---|---|---|
label | string | ReactNode | Display name in the "Add block / field / action" menu. Supports tExpr() for deferred translation |
icon | ReactNode | Icon in the menu |
sort | number | Sort weight; lower numbers appear first. Default is 0 |
hide | boolean | (ctx) => boolean | Whether to hide in the menu. Supports dynamic evaluation |
group | string | Group identifier for categorizing into specific menu groups |
children | SubModelItem[] | (ctx) => SubModelItem[] | Sub-menu items. Supports async functions for dynamic construction |
toggleable | boolean | (model) => boolean | Whether toggle behavior is supported (unique within the same parent) |
searchable | boolean | Whether sub-menu search is enabled |
Most plugins only need to set label:
MyBlockModel.define({
label: tExpr('My block'),
});
If you need to control sorting or visibility, add sort and hide:
MyBlockModel.define({
label: tExpr('My block'),
sort: 10, // Appears later in the list
hide: (ctx) => !ctx.someCondition, // Conditionally hidden
});
NocoBase provides multiple FlowModel base classes. Choose based on what you're extending:
| Base Class | Purpose | Documentation |
|---|---|---|
BlockModel | Basic block | Block Extension |
DataBlockModel | Block that fetches its own data | Block Extension |
CollectionBlockModel | Bound to a collection, auto-fetches data | Block Extension |
TableBlockModel | Full table block with field columns, action bar, etc. | Block Extension |
FieldModel | Field component | Field Extension |
ActionModel | Action button | Action Extension |
Generally speaking, use TableBlockModel for table blocks (most common, out-of-the-box), CollectionBlockModel or BlockModel for fully custom rendering, FieldModel for fields, and ActionModel for action buttons.