Create Custom Node
Build a reusable Codemation node with `defineNode(...)`, then drop to classes only when you need more control.
Create Custom Node
This guide starts with the lightweight defineNode(...) helper, which is the fastest path for app-local or package-local nodes.
Use this guide when
Prefer a custom node when:
- the built-in nodes are not enough
- a
Callbackis becoming real product logic - you want isolated tests and reuse across multiple workflows
Example scenario
Imagine you want a node that uppercases a field called subject.
The lightweight pattern is:
- define the node once with
defineNode(...) - implement
execute(args, context)so the engine runs your logic once per item - keep reusable node params on config and let workflows map those params with literals or
itemExpr(...) - use the helper directly from a fluent workflow
If you need legacy batch semantics (one function over the whole Items array), use defineBatchNode(...) with run(items, ...) instead.
Step 1: define the node
import { defineNode } from "@codemation/core";
export const uppercaseNode = defineNode({
key: "example.uppercase",
title: "Uppercase field",
input: {
field: "string",
},
execute({ input }, { config }) {
return {
...input,
[config.field]: String(input[config.field as keyof typeof input] ?? "").toUpperCase(),
};
},
});Step 2: use the node in a workflow
import { workflow } from "@codemation/host";
export default workflow("wf.uppercase.subject")
.name("Uppercase subject")
.manualTrigger({ subject: "hello" })
.node(uppercaseNode, { field: "subject" })
.build();Params can be expressions
defineNode(...) config is the node's parameter surface. Workflows can pass either:
- literal values such as
{ field: "subject" } - per-item expressions such as
itemExpr(({ item }) => ...)
The engine resolves itemExpr(...) before execute(...) runs, so node code still sees plain resolved values:
export const invoiceNode = defineNode({
key: "example.invoice",
title: "Invoice OCR",
input: {
binaryField: "data",
},
execute({ item }, { config }) {
const attachment = item.binary?.[config.binaryField];
if (!attachment) {
throw new Error(`Missing binary attachment at key "${config.binaryField}".`);
}
return { attachmentId: attachment.id };
},
});That keeps reusable nodes independent from the surrounding workflow item shape while still letting each workflow map upstream data into the node's params.
When to drop down to classes
Reach for the lower-level RunnableNodeConfig<TIn, TOut> + Node<TConfig> pattern when you need:
- constructor-injected collaborators
- decorators and persisted runtime metadata
- more explicit packaging boundaries for published plugins
The class-based pattern is still fully supported. defineNode(...) is simply the better place to start.
Why this pattern pays off
Compared with burying logic inside a callback, a custom node gives you:
- clearer names in the workflow graph
- isolated tests
- reuse across workflows and apps
- a stable place for credential and dependency boundaries