Codemation Docs
Workflows

Custom Nodes

Extend Codemation with your own nodes by pairing a config class with a node implementation class.

… stars

When the built-in nodes are not enough, define your own node.

The built-in Callback node is fine for quick, inline logic when you want to move fast. The intended way to build durable workflow behavior is still a custom node (config + implementation): that is what makes automated testing straightforward and reuse natural. If you are past a one-off experiment, prefer this page over piling logic into Callback.

The pattern is simple:

  1. a config class that describes how the node is used in workflow definitions
  2. a node implementation class that performs the runtime work

Where custom nodes can live

You do not need a separate package for every custom node.

There are two good options:

  • inside your app repo when the node is specific to a single app
  • inside a reusable package when you want to share or publish it

If a node only exists for one app, keeping it in the app repo is often the best choice.

For example, the sample app in this repository has app-local custom nodes under:

src/nodes/
  kitchenSinkExample.ts
  kitchenSinkExampleNode.ts

Use a separate package when:

  • the node should be reused across multiple apps
  • you want a cleaner package boundary
  • you plan to publish or version the node independently

Config class example

The @codemation/node-example package contains a compact example:

export class ExampleUppercase<
  TInputJson extends Record<string, unknown> = Record<string, unknown>,
  TField extends keyof TInputJson & string = keyof TInputJson & string,
> implements RunnableNodeConfig<TInputJson, TInputJson> {
  readonly kind = "node" as const;
  readonly type: TypeToken<unknown> = ExampleUppercaseNode;

  constructor(
    public readonly name: string,
    public readonly cfg: { field: TField },
    public readonly id?: string,
  ) {}
}

The important parts are:

  • kind = "node"
  • type pointing at the implementation class
  • constructor arguments that become workflow configuration

Node implementation example

The runtime class processes items in batches:

@node({ packageName: "@codemation/node-example" })
export class ExampleUppercaseNode implements Node<ExampleUppercase<Record<string, unknown>, string>> {
  kind = "node" as const;
  outputPorts = ["main"] as const;

  async execute(items: Items, ctx: NodeExecutionContext<ExampleUppercase<Record<string, unknown>, string>>) {
    const out: Item[] = [];
    for (const item of items) {
      const json = typeof item.json === "object" && item.json !== null ? (item.json as Record<string, unknown>) : {};
      const value = String(json[ctx.config.cfg.field] ?? "");
      out.push({ ...item, json: { ...json, [ctx.config.cfg.field]: value.toUpperCase() } });
    }
    return { main: out };
  }
}

The same pattern also works inside an app repo. A custom node does not need a package name unless you are treating it as a reusable package.

What to remember

  • Nodes receive batches of items, not single records
  • Keep constructor setup cheap and side-effect free
  • Put external calls inside execute(...)
  • Prefer explicit configuration over hidden globals
  • Return outputs by named ports such as main

App-local node vs shared package

If you keep the node in the app repo, a structure like this works well:

src/
  nodes/
    MyCustomNode.ts
    MyCustomNodeConfig.ts

For anything reusable, prefer a separate package such as:

packages/node-my-feature/
  src/
    MyNodeConfig.ts
    MyNode.ts

That gives you a clean path to share nodes across projects.

Register app-local nodes

If the node lives inside your app repo, register the runtime class in codemation.config.ts.

import type { CodemationAppContext, CodemationConfig } from "@codemation/host";

export default {
  register(app: CodemationAppContext) {
    app.registerNode(MyCustomNode);
    app.discoverWorkflows("src/workflows");
  },
} satisfies CodemationConfig;

Without that registration step, the host knows about the config class in your workflow code but does not know how to construct the runtime implementation.

If the node lives in a plugin package, do the same registration from definePlugin({ register(context) { ... } }).

Use a custom node in a workflow

Once the package is installed and available, use it like any other node:

new ExampleUppercase("Uppercase subject", { field: "subject" });

For an app-local node, the usage looks the same from the workflow author’s point of view:

new MyCustomNodeConfig("Enrich invoice", { customerIdField: "customerId" });

Good first custom node ideas

  • domain-specific data normalization
  • API wrappers with opinionated defaults
  • CRM or ERP helpers
  • prompt-preparation steps
  • validation or enrichment nodes

Custom nodes as AI tools

If you already have a good runnable node, you can also expose it to AIAgent as a node-backed tool instead of writing a second custom Tool adapter.

That is usually the right choice when:

  • the node already has the correct DI wiring
  • the node already declares credential requirements
  • the agent should reuse the same business capability a normal workflow can call directly

See Plugin developers for the node-backed tool pattern and AgentToolFactory.asTool(...).

Continue with

  1. Create custom credential
  2. Use a node as an agent tool
  3. Plugin developers

On this page