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.