FlowEngine

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.

20260403215904

For plugin developers, FlowEngine provides two core concepts:

  • FlowModel — A configurable component model responsible for rendering UI and managing props
  • Flow — A configuration flow that defines the component's configuration panel and data processing logic

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.

What is 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.

Minimal Example

Creating and registering a FlowModel takes three steps:

1. Extend a Base Class and Implement renderComponent

// 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.

2. Register in the Plugin

// 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'),
      },
    });
  }
}

3. Use in the Interface

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".

20260403221815

Adding Configuration with registerFlow

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 rendering
  • uiSchema — Defines the configuration panel UI in JSON Schema format. See UI Schema for syntax reference
  • handler(ctx, params)params contains the values the user entered in the configuration panel, which are set to the model via ctx.model.props
  • defaultParams — Default values for the configuration panel

Common uiSchema Patterns

uiSchema 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.

define() Parameter Reference

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:

ParameterTypeDescription
labelstring | ReactNodeDisplay name in the "Add block / field / action" menu. Supports tExpr() for deferred translation
iconReactNodeIcon in the menu
sortnumberSort weight; lower numbers appear first. Default is 0
hideboolean | (ctx) => booleanWhether to hide in the menu. Supports dynamic evaluation
groupstringGroup identifier for categorizing into specific menu groups
childrenSubModelItem[] | (ctx) => SubModelItem[]Sub-menu items. Supports async functions for dynamic construction
toggleableboolean | (model) => booleanWhether toggle behavior is supported (unique within the same parent)
searchablebooleanWhether 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
});

FlowModel Base Class Selection

NocoBase provides multiple FlowModel base classes. Choose based on what you're extending:

Base ClassPurposeDocumentation
BlockModelBasic blockBlock Extension
DataBlockModelBlock that fetches its own dataBlock Extension
CollectionBlockModelBound to a collection, auto-fetches dataBlock Extension
TableBlockModelFull table block with field columns, action bar, etc.Block Extension
FieldModelField componentField Extension
ActionModelAction buttonAction 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.