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.tssrc/nodes/<type>/index.tsx- optional
src/nodes/<type>/form-meta.tsx src/nodes/index.tssrc/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:
startendllmhttpcodetoolknowledgevariableconditionloop
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:
typeinfometaonAddformMeta
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:
titleis requiredinputsValues.*is validated against required fields
5.2 It already carries a set of key effects
Including:
syncVariableTitleprovideJsonSchemaOutputsautoRenameRefEffectvalidateWhenVariableSynclistenRefSchemaChange
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.requiredinputs.propertiesinputsValuesoutputs.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:
ToolNodeRegistryuses the default form meta and a clear input/output contractKnowledgeNodeRegistrysurfaces 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.