Skip to main content

Overview

Creating a custom node involves defining Zod schemas for inputs and outputs, implementing an executor function, and registering the node with the registry.
1
Define Schemas with Zod
2
import { z } from 'zod';

const MyInputSchema = z.object({
  query: z.string(),
  limit: z.number().min(1).max(100).default(10),
  includeMetadata: z.boolean().optional(),
});

const MyOutputSchema = z.object({
  results: z.array(z.object({
    id: z.string(),
    title: z.string(),
    score: z.number(),
  })),
  totalCount: z.number(),
});
3
Schema design tips:
  • .optional() for non-required fields
  • .default(value) for defaults (rendered as optional in generated forms)
  • z.enum([...]) for dropdowns in generated forms
  • .describe('...') for field descriptions (used by schema introspector)
  • Nested z.object() creates collapsible sections in the config panel
4
Create the Node with defineNode
5
import { defineNode } from '@jam-nodes/core';

export const mySearchNode = defineNode({
  type: 'my_search',
  name: 'My Search',
  description: 'Search for items matching a query',
  category: 'integration',
  inputSchema: MyInputSchema,
  outputSchema: MyOutputSchema,
  estimatedDuration: 10,
  capabilities: {
    supportsRerun: true,
    supportsCancel: true,
  },
  executor: async (input, context) => {
    try {
      const results = await doSearch(input.query, input.limit);
      return {
        success: true,
        output: { results, totalCount: results.length },
      };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Search failed',
      };
    }
  },
});
6
Choose a Category
7
CategoryUse ForactionAI generation, email drafting, content creationlogicConditionals, delays, flow control, end markersintegrationExternal APIs (HTTP, Apollo, DataForSEO, social platforms)transformData manipulation (map, filter, reshape)
8
Use Credentials
9
Nodes access API credentials via context.credentials. Always check availability and make direct HTTP calls:
10
executor: async (input, context) => {
  const apiKey = context.credentials?.apollo?.apiKey;
  if (!apiKey) {
    return { success: false, error: 'Apollo API key not configured.' };
  }

  const response = await fetch('https://api.apollo.io/api/v1/mixed_people/search', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': apiKey,
    },
    body: JSON.stringify({ person_titles: input.titles }),
  });

  const data = await response.json();
  return { success: true, output: { contacts: data.people } };
}
11
Available credentials:
12
ServiceCredential FieldsapolloapiKeytwitterbearerToken, twitterApiIoKeyforumScoutapiKeydataForSeoapiToken (Base64 encoded)openaiapiKeyanthropicapiKey
13
The host application provides credentials when executing nodes. For the playground CLI, credentials are loaded from environment variables or the encrypted credential store.
14
Use ExecutionContext Features
15
executor: async (input, context) => {
  // User and workflow info
  context.userId;
  context.workflowExecutionId;
  context.campaignId;

  // Access upstream node outputs
  const prevData = context.variables['previousNodeId'];
  const nested = context.resolveNestedPath('contacts[0].email');

  return {
    success: true,
    output: { /* ... */ },

    // Optional: conditional branching
    nextNodeId: someCondition ? 'nodeA' : 'nodeB',

    // Optional: request approval
    needsApproval: {
      resourceIds: ['id1'],
      resourceType: 'email_draft',
      message: 'Please review',
    },

    // Optional: notification
    notification: {
      title: 'Complete',
      message: 'Found 42 results',
    },
  };
}
16
Register the Node
17
import { NodeRegistry } from '@jam-nodes/core';
import { builtInNodes } from '@jam-nodes/nodes';
import { mySearchNode } from './my-search-node';

const registry = new NodeRegistry();
registry.registerAll(builtInNodes);
registry.register(mySearchNode);

// Test with the CLI playground
// jam run my_search --mock
18
Test the Node
19
import { createExecutionContext } from '@jam-nodes/core';
import { mySearchNode } from './my-search-node';

const ctx = createExecutionContext({ previousData: 'hello' });
const nodeContext = ctx.toNodeContext('test-user', 'test-run');
nodeContext.services = { /* mock services */ } as any;

const result = await mySearchNode.executor(
  { query: 'test', limit: 5 },
  nodeContext
);

expect(result.success).toBe(true);
expect(result.output?.totalCount).toBeGreaterThan(0);

// Schema validation
const validated = mySearchNode.inputSchema.parse({ query: 'test' });
expect(() => mySearchNode.inputSchema.parse({})).toThrow();

File Organization

packages/nodes/src/
  my-category/
    my-node.ts          # Node definition + schemas
    index.ts            # Re-exports
  index.ts              # Add to builtInNodes array + re-export
For AI nodes with prompts:
schemas/ai.ts           # Zod schemas shared between nodes
prompts/my-node.ts      # Prompt templates and helpers
ai/my-node.ts           # Node definition using schemas + prompts