Skip to main content

Frontend Custom Node Development

This page covers only the frontend half; it does not cover the backend executor.

Where custom-nodes.md describes the end-to-end contract between frontend and backend, this page covers how — inside ai4j-flowgram-webapp-demo — a new node actually becomes an "editable, validatable, serializable, backend-mappable" frontend node.

1. First, get clear on what a frontend node is actually made of​

In the current demo, a node typically involves at least:

  • src/nodes/constants.ts
  • src/nodes/<type>/index.tsx
  • optional src/nodes/<type>/form-meta.tsx
  • src/nodes/index.ts
  • src/utils/backend-workflow.ts

Each layer solves a different problem:

  • What the type is
  • What the node looks like and what its initial data is
  • How the right-hand form renders and validates
  • Whether the editor recognizes the node
  • How it maps when sent to the backend

Miss any one layer and the node is only half-finished.

2. Step one: define the frontend type first​

The current frontend enum lives in:

  • ai4j-flowgram-webapp-demo/src/nodes/constants.ts

Existing types include:

  • start
  • end
  • llm
  • http
  • code
  • tool
  • knowledge
  • variable
  • condition
  • loop

If you are adding a custom node, the first step is to turn it into an official frontend type, for example:

export enum WorkflowNodeType {
// ...
Transform = 'transform',
}

This step looks trivial, but it actually defines the protocol name on the editor side.

3. Step two: write the node registry​

What actually hands the node to the editor for recognition is FlowNodeRegistry.

The current registry cares at minimum about:

  • type
  • info
  • meta
  • onAdd
  • formMeta

3.1 type​

Determines which kind of node this is.

3.2 info​

Determines the node's icon and description in the node panel.

3.3 meta​

Determines editor metadata such as the default node size.

3.4 onAdd​

This is the most critical part. It decides what JSON gets generated by default when a node is dragged onto the canvas.

3.5 formMeta​

Determines how the right-hand form panel renders, validates, and reacts.

4. onAdd() is really your frontend schema factory​

Many people read onAdd() as "just adds a node to the canvas." That is not precise enough.

More precisely:

onAdd() is responsible for generating the node's initial schema, input/output constraints, and default bound values.

For example, a minimal TRANSFORM node:

import { nanoid } from 'nanoid';
import { FlowNodeRegistry } from '../../typings';
import { WorkflowNodeType } from '../constants';
import { defaultFormMeta } from '../default-form-meta';

let index = 0;

export const TransformNodeRegistry: FlowNodeRegistry = {
type: WorkflowNodeType.Transform,
info: {
icon: '/icons/transform.svg',
description: 'Normalize text and return a transformed result.',
},
meta: {
size: { width: 360, height: 320 },
},
onAdd() {
return {
id: `transform_${nanoid(5)}`,
type: WorkflowNodeType.Transform,
data: {
title: `Transform_${++index}`,
inputsValues: {
text: {
type: 'template',
content: '',
},
mode: {
type: 'constant',
content: 'upper',
},
},
inputs: {
type: 'object',
required: ['text'],
properties: {
text: { type: 'string' },
mode: { type: 'string' },
},
},
outputs: {
type: 'object',
required: ['result'],
properties: {
result: { type: 'string' },
},
},
},
};
},
formMeta: defaultFormMeta,
};

This example has effectively defined:

  • The node protocol name
  • The form defaults
  • The input schema
  • The output schema

5. Why defaultFormMeta is worth reusing first​

Several nodes in the current demo reuse directly:

  • src/nodes/default-form-meta.tsx

This default meta is more useful than it looks.

5.1 It already ships with basic validation​

Including:

  • title is required
  • inputsValues.* is validated against required fields

5.2 It already carries a set of key effects​

Including:

  • syncVariableTitle
  • provideJsonSchemaOutputs
  • autoRenameRefEffect
  • validateWhenVariableSync
  • listenRefSchemaChange

This means the default form panel is not just "able to accept text input"; it already handles for you:

  • Title syncing
  • Output schema derivation
  • Reference rename linkage
  • Variable reference validation
  • Reference schema change listening

5.3 Practical advice​

If your new node does not have a strongly differentiated interaction, reuse defaultFormMeta first, and only split out a dedicated form-meta.tsx once you confirm you genuinely need custom interaction.

6. Step three: register the node with the editor​

Defining the registry alone is not enough; you also have to add it to:

  • ai4j-flowgram-webapp-demo/src/nodes/index.ts

The current nodeRegistries is the actual source the editor uses to recognize nodes.

If you skip registration, you hit very typical symptoms:

  • The node definition clearly exists in the code
  • But it does not show up in the node panel
  • Existing workflow JSON also fails to recognize that type

7. Step four: the frontend/backend type mapping must be handled​

This step gets missed often.

The frontend does not hand the raw node type to the backend as-is; instead it goes through:

  • backend-workflow.ts

If your frontend type is:

  • transform

And the backend executor type is:

  • TRANSFORM

Then you must add it to:

const BACKEND_TYPE_MAP: Record<string, string> = {
transform: 'TRANSFORM',
};

Otherwise everything looks fine on the frontend, but the backend will report at the validation stage:

  • unsupported node type

8. What the frontend must really align on is not appearance, but schema​

Whether a frontend custom node is up to standard does not depend on how pretty the card looks; it depends on whether these sets of data are stable:

  • inputs.required
  • inputs.properties
  • inputsValues
  • outputs.properties

These fields determine:

  • How the form validates
  • What input the runtime receives
  • What output downstream nodes can reference

If these fields are defined loosely, no amount of backend executor strength will help.

9. What is most worth learning when staying consistent with built-in nodes​

Looking at the current built-in node registries, the most worthwhile thing to reuse is not the styling, but how they are organized:

  • ToolNodeRegistry uses the default form meta and a clear input/output contract
  • KnowledgeNodeRegistry surfaces backend-critical fields such as serviceId, embeddingModel, namespace, and query up front

This shows that good frontend node definitions share a common trait:

  • They expose the contract the backend actually needs as explicit form fields

Rather than hiding complex logic inside the frontend.

10. The most common frontend mistakes​

10.1 Added the type but forgot the backend map​

Result:

  • The node can be dragged onto the canvas
  • But the backend does not recognize it

10.2 inputs and inputsValues are out of alignment​

Result:

  • The form looks filled in
  • But the runtime still reports required missing

10.3 The output schema is written too loosely​

Result:

  • The downstream node's reference path is unstable
  • Both the form and the display panel become hard to build

10.4 Jumping straight into a complex dedicated form meta​

Result:

  • UI complexity spirals out of control first
  • While the node contract is still not stable

11. The single most important criterion​

Whether a frontend custom node is well-written is not judged by how flashy its JSX is, but by these four points:

  • Dragging it onto the canvas produces a stable initial schema
  • Form validation is consistent with the required fields
  • It maps correctly to a type the backend recognizes
  • The output schema is stable enough for downstream references

Only when all four are satisfied is it the frontend half of an official platform node.