Custom Nodes
Build reusable workflow nodes with the Flow SDK — define typed inputs/outputs, write execution logic, test, and publish to the node palette.
Custom nodes extend the Workflow Studio canvas with your own business logic. Once built and deployed, a custom node appears in the Node Palette under the Custom category and can be dropped into any workflow like a built-in node. Build them at /workflows/custom-nodes/new.
Creating a Custom Node
Navigate to Workflows → Custom Nodes → New Custom Node. The builder has three sections:
- •Metadata panel (left) — Name, slug, icon, color, description, and tags.
- •Schema editor (center) — Define input and output fields with types, validation, and description.
- •Code editor (right) — Write the TypeScript execution function with full IntelliSense.
Input/Output Schema
{
"inputs": [
{
"name": "incidentId",
"label": "Incident ID",
"type": "string",
"required": true,
"description": "The ID of the incident to enrich.",
"expression": true
},
{
"name": "mode",
"label": "Enrichment Mode",
"type": "string",
"required": false,
"default": "basic",
"enum": ["basic", "full"],
"description": "Level of enrichment to perform."
}
],
"outputs": [
{
"name": "ciCount",
"label": "CI Count",
"type": "number",
"description": "Number of CIs linked to the incident."
},
{
"name": "riskScore",
"label": "Risk Score",
"type": "number",
"description": "Calculated risk score (0-100)."
},
{
"name": "enriched",
"label": "Enriched",
"type": "boolean",
"description": "Whether enrichment succeeded."
}
]
}Execution Function
import type { NodeContext, CustomNodeInput } from '@flowos/sdk'
interface MyInput {
incidentId: string
mode: 'basic' | 'full'
}
interface MyOutput {
ciCount: number
riskScore: number
enriched: boolean
}
export async function run(ctx: NodeContext, input: CustomNodeInput<MyInput>): Promise<MyOutput> {
const incident = await ctx.tables.find('incidents', input.incidentId)
const cis = incident.cmdb_items ?? []
const riskScore = cis.length * 10 + (incident.severity === 'P1' ? 50 : 20)
if (input.mode === 'full') {
await ctx.tables.update('incidents', incident.id, {
custom_fields: { ...incident.custom_fields, risk_score: riskScore },
})
}
return { ciCount: cis.length, riskScore, enriched: true }
}Using a Custom Node in the Canvas
Once deployed, the node appears in the palette under Custom. Drag it onto the canvas. The right-side config panel shows your declared input fields, each bindable to upstream expressions.
{
"id": "enrich-1",
"type": "custom:enrich-incident",
"config": {
"incidentId": "{{trigger.record.id}}",
"mode": "full"
}
}
// Downstream nodes can access:
// {{nodes.enrich-1.ciCount}}, {{nodes.enrich-1.riskScore}}, {{nodes.enrich-1.enriched}}Testing Custom Nodes
Test a custom node at /workflows/custom-nodes/[id] → Tests tab. See Testing Artifacts for the full test API.
Versioning & Deployment
- •Every save creates a new version. The node palette always shows the latest deployed version.
- •In-progress workflow runs that are using an older version of the node continue to completion with that version.
- •If you need to push a breaking input/output change, increment the node's slug version (
enrich-incident-v2) so existing workflows usingenrich-incidentdon't break. - •Deprecated nodes show a warning banner in the canvas: "This node has been deprecated. Replace with enrich-incident-v2."
Packaging for Reuse
Custom nodes can be bundled into a Pack for distribution across workspaces or publishing to the marketplace.