Codemation Docs
How-To Guides

Create Custom Node

Build a reusable Codemation node with `defineNode(...)`, then drop to classes only when you need more control.

… stars

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 Callback is 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:

  1. define the node once with defineNode(...)
  2. implement execute(args, context) so the engine runs your logic once per item
  3. keep reusable node params on config and let workflows map those params with literals or itemExpr(...)
  4. 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
  1. Create a custom credential
  2. Use a node as an agent tool
  3. Callback node

On this page