For AI agents: the complete documentation index is available at https://a3s-lab.github.io/Flow/v1.0.0/en/llms.txt, the full documentation bundle is available at https://a3s-lab.github.io/Flow/v1.0.0/en/llms-full.txt, and this page is available as Markdown at https://a3s-lab.github.io/Flow/v1.0.0/en/reference/custom-nodes.md.

Custom nodes

A custom node adds a host capability to the workflow editor without changing the built-in catalog. The host owns the manifest, executor, authorization, and release process. Flow UI owns the typed graph contract and the adapter from that manifest to an A3S UI form.

Use a custom node when the operation has a stable business meaning that deserves its own ports and settings. Keep occasional work in flow.step with a registered task name. A separate node is useful when authors need a recognizable card, a constrained configuration form, or typed data ports.

Ownership boundary

ConcernOwner
Form document, compilation, validation, layout, and native controlsA3S UI Form
Workflow manifest adapter and workflow-specific composite controlsFlow UI
Custom node name, fields, ports, and capability declarationHost application
Handler lookup, credentials, permissions, queues, and side effectsHost runtime
Final publication decisionHost release pipeline

The configuration panel uses FormRenderer from @a3s-lab/ui/form/react. Ordinary text, number, select, switch, slider, password, tags, and textarea fields reach A3S UI through NativeWidget. Flow UI supplies composite editors for workflow expressions, schemas, batches, child scopes, prompts, structured JSON, durations, and ordered lists.

The package test matrix renders every registered node and rejects visible inputs, selects, or textareas that leave the A3S UI form contract. Composite editors may own workflow-specific state and layout, but their atomic controls still use the A3S UI input, select, textarea, and button primitives.

Define one registration

Register the manifest and its executor capability together. This prevents the editor from advertising a node that the release pipeline cannot authorize.

import {
  createA3SFlowDagNodeCatalog,
  defineA3SFlowCustomDagNode,
} from '@a3s-lab/flow-ui';

const scoreOrder = defineA3SFlowCustomDagNode({
  manifest: {
    type: 'commerce.risk.score',
    display_name: 'Score order risk',
    description: 'Score an order with the host risk service.',
    category: 'custom',
    categoryLabel: 'Custom nodes',
    role: 'host',
    icon: 'shield-check',
    ports: {
      inputs: [
        { id: 'in', label: 'In', kind: 'control', types: ['FlowControl'] },
        { id: 'order', label: 'Order', kind: 'data', types: ['Json'] },
      ],
      outputs: [
        { id: 'next', label: 'Next', kind: 'control', types: ['FlowControl'] },
        { id: 'score', label: 'Score', kind: 'data', types: ['Number'] },
      ],
    },
    input_types: ['Json'],
    output_types: ['Number'],
    fields: [
      {
        name: 'policy',
        display_name: 'Scoring policy',
        info: 'Choose a policy already authorized by the host.',
        type: 'str',
        _input_type: 'DropdownInput',
        value: 'balanced-v2',
        options: [
          { label: 'Balanced policy v2', value: 'balanced-v2' },
          { label: 'Strict review', value: 'strict-v1' },
        ],
        required: true,
      },
      {
        name: 'review_threshold',
        display_name: 'Review threshold',
        info: 'Send scores at or above this value to manual review.',
        type: 'slider',
        _input_type: 'SliderInput',
        value: 0.72,
        range_spec: { min: 0, max: 1, step: 0.01 },
        required: true,
      },
      {
        name: 'strict_validation',
        display_name: 'Strict validation',
        info: 'Enable this to stop when required order fields are missing.',
        type: 'bool',
        _input_type: 'BoolInput',
        value: false,
      },
    ],
    outputs: [
      {
        name: 'score',
        display_name: 'Score',
        types: ['Number'],
        group_outputs: false,
        allows_loop: false,
        tool_mode: false,
      },
    ],
  },
  capability: {
    id: 'commerce/risk-score',
    version: '1.2.3',
    handler: 'risk.score-order',
  },
});

export const flowCatalog = createA3SFlowDagNodeCatalog([scoreOrder]);

The type needs at least three lowercase namespace segments. flow.*, iteration, loop, and their internal start types are reserved. A custom manifest must be public, use the host role, and cannot claim a Flow runtime command or container binding.

The capability ID must be namespaced. Its version is one exact semantic version, not a range. The handler is the stable key that the host runtime maps to executable code. Registration validates all three values and rejects collisions with built-in or previously registered types.

createA3SFlowDagNodeCatalog returns a new immutable registry. It never changes a3sFlowDagNodeRegistry, so tests and editors can create isolated catalogs without leaking state into another application.

Field controls

Choose the simplest field type that describes the value. The manifest remains the source of truth for defaults, required values, enum choices, ranges, groups, and conditional visibility.

Manifest inputRendered controlNotes
StrInputA3S UI text inputAdd multiline only for longer prose
DropdownInputA3S UI selectPut stable machine values in options
IntInput, FloatInputA3S UI number inputUse range_spec for bounds and step
BoolInputA3S UI switchUse a direct action label
SliderInputA3S UI sliderGive the user a meaningful min, max, and step
MultilineInputA3S UI textareaSuitable for notes, not structured data
JSONInputStructured JSON editorThe field schema remains an object
DurationInputAmount and unit controlStore { value, unit } as the default
PromptInputPrompt editorSupports workflow variable insertion
SortableListInputOrdered list editorUse stable option values
A3SFlowExpressionInputFlow expression editorUse for replay-safe references and operators
A3SFlowSchemaInputFlow schema editorUse for workflow input and output contracts
A3SFlowSpecInputWorkflow spec editorPins workflow, runtime, entrypoint, and export
A3SFlowChildrenInputChild workflow editorKeeps 1 to 64 unique child definitions in order

validateA3SFlowDagNodeConfiguration first compiles the generated A3S UI form and validates the value against its plan. It then checks every Flow expression contract, date-time and token purpose, JSON Schema root, duration unit, workflow spec, and child-workflow member. Built-in nodes add command-specific checks such as retry bounds, stable callback tokens, unique batch keys, and connected failure ports. These checks apply to custom manifests as well; rules that depend on the host service or tenant still belong to the host.

Use the catalog in an editor

Pass the same registry to node creation, hooks, canvas cards, configuration panels, connection validation, and serialization. Mixing the built-in singleton with a custom catalog produces an editor where a node may appear in one surface and fail in another.

import {
  A3SFlowDagNodeConfigurationPanel,
  A3SFlowDagNodePreview,
  useA3SFlowNode,
} from '@a3s-lab/flow-ui/react';
import { flowCatalog } from './flow-catalog';

export function RiskNodeEditor() {
  const state = useA3SFlowNode({
    id: 'score-order',
    type: 'commerce.risk.score',
    registry: flowCatalog.registry,
  });

  return (
    <>
      <A3SFlowDagNodePreview
        dagNode={state.node}
        registry={flowCatalog.registry}
      />
      <A3SFlowDagNodeConfigurationPanel
        dagNode={state.node}
        onChange={state.setNode}
        registry={flowCatalog.registry}
      />
    </>
  );
}

The Vue useA3SFlowNode composable accepts the same registry option. A ref or getter can provide it when the host changes catalogs between projects.

Require capability binding at publication

Structural compilation deliberately treats data.type as opaque. Publication adds host policy and must use the catalog.

import { compileA3SFlowWorkflowDagForPublication } from '@a3s-lab/flow-ui';
import { flowCatalog } from './flow-catalog';

const result = compileA3SFlowWorkflowDagForPublication(
  document.workflow.graph,
  flowCatalog.registry,
  flowCatalog.capabilities,
);

if (!result.ok) {
  throw new Error(JSON.stringify(result.issues));
}

The publication gate rejects unregistered types, top-level internal nodes, missing capability bindings, mismatched node types, malformed capability IDs, version ranges, and empty handlers. It proves that the document points at an admitted handler identity. The host still decides whether that handler is installed, permitted for the project, and allowed to use its credentials.

CLI and Skill boundary

The packaged a3s-flow CLI contains the official built-in catalog and does not load application code. It will report a custom type as unknown. A project that publishes custom nodes should add a small typed validation command that imports its catalog and calls compileA3SFlowWorkflowDagForPublication.

The packaged A3S Flow Skill follows the same rule. It may query and edit built-in nodes through the CLI. Give an agent access to a custom node only through project documentation that names the catalog module, supported types, capability owner, and project validation command. This keeps the agent from inventing a handler or bypassing release authorization.

Common failures

ErrorRepair
node_type_unregisteredImport the project catalog and pass its registry everywhere
capability_binding_missingRegister the node and capability as one defineA3SFlowCustomDagNode value
capability_binding_invalidUse the exact node type, namespaced capability ID, exact version, and nonempty handler
manifest_form_invalidCorrect the manifest field type, default, options, or widget contract
Form value errorRepair the named field and preserve the rest of the node data