Skip to main content

Custom Workflow Nodes

Custom Workflow nodes let you add new node types to the visual Workflow builder. Use them to encapsulate business logic, integrate with proprietary systems, or extend Convotic's automation capabilities.

Registering a node handler

Use sdk.registerNodeHandler() inside your module's register function.

Basic example

import { ConvoticModule } from "@convotic/block-sdk";

const module: ConvoticModule = {
name: "loyalty-module",
version: "1.0.0",

register(sdk) {
sdk.registerNodeHandler({
type: "loyalty_lookup",
name: "Loyalty Lookup",
description: "Look up a customer's loyalty points and tier.",
category: "data",
icon: "star",

// Define the fields that appear in the node's configuration panel
inputs: [
{
key: "customerId",
label: "Customer ID",
type: "string",
required: true,
description: "The customer ID to look up. Supports {{variables}}.",
},
],

// Define the output variables this node produces
outputs: [
{ key: "points", type: "number" },
{ key: "tier", type: "string" },
{ key: "memberSince", type: "string" },
],

handler: async (inputs, ctx) => {
const { customerId } = inputs;

// Call your loyalty API
const response = await fetch(
`https://loyalty.example.com/api/members/${customerId}`,
{
headers: {
Authorization: `Bearer ${ctx.secrets.get("LOYALTY_API_KEY")}`,
},
}
);

if (!response.ok) {
throw new Error(`Loyalty API returned ${response.status}`);
}

const member = await response.json();

return {
points: member.points,
tier: member.tier,
memberSince: member.created_at,
};
},
});
},
};

export default module;

Once deployed, the Loyalty Lookup node appears in the Workflow builder under the Data category. Users can drag it onto the canvas, configure the Customer ID field, and reference the output variables ({{loyalty_lookup.points}}, {{loyalty_lookup.tier}}) in downstream nodes.

Node handler signature

sdk.registerNodeHandler({
type: string, // Unique identifier for the node type
name: string, // Display name in the builder
description: string, // Tooltip / help text
category: "messaging" | "logic" | "data" | "advanced",
icon?: string, // Icon name from the Convotic icon set

inputs: InputDefinition[],
outputs: OutputDefinition[],

handler: (inputs: Record<string, any>, ctx: NodeContext) => Promise<Record<string, any>>,
});

InputDefinition

PropertyTypeDescription
keystringThe input field key.
labelstringDisplay label in the configuration panel.
type"string" | "number" | "boolean" | "select" | "json"The input field type.
requiredbooleanWhether the field is required.
descriptionstringHelp text shown below the field.
options{ label: string; value: string }[]Options for select type inputs.
defaultanyDefault value.

OutputDefinition

PropertyTypeDescription
keystringThe output variable key.
type"string" | "number" | "boolean" | "object" | "array"The output value type.

NodeContext

PropertyDescription
ctx.contactsContact CRUD operations.
ctx.conversationsConversation CRUD operations.
ctx.messagesSend messages to contacts.
ctx.storageKey-value storage scoped to the module.
ctx.secretsAccess workspace secrets (API keys, tokens).
ctx.loggerStructured logger.
ctx.executionCurrent Workflow execution metadata.

Example: condition node

Custom nodes can also control flow by returning a branch property:

sdk.registerNodeHandler({
type: "credit_check",
name: "Credit Check",
description: "Check if a customer's credit score meets a threshold.",
category: "logic",

inputs: [
{ key: "customerId", label: "Customer ID", type: "string", required: true },
{ key: "threshold", label: "Minimum Score", type: "number", required: true, default: 700 },
],

outputs: [
{ key: "score", type: "number" },
{ key: "branch", type: "string" },
],

handler: async (inputs, ctx) => {
const score = await getCreditScore(inputs.customerId);

return {
score,
branch: score >= inputs.threshold ? "approved" : "denied",
};
},
});

In the Workflow builder, the node will display two output branches (approved and denied) that you can connect to different downstream nodes.

Error handling

If your handler throws an error, the Workflow execution follows the standard retry policy. You can also return an error state explicitly:

handler: async (inputs, ctx) => {
try {
const result = await externalApiCall(inputs);
return { data: result };
} catch (error) {
ctx.logger.error("External API call failed", { error: error.message });
throw new Error(`Node failed: ${error.message}`);
}
}
tip

Use ctx.secrets.get() to access sensitive values like API keys. Never hardcode credentials in your node handler.