Workflow Studio

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

json
{
  "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

typescript
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.

Canvas node config (auto-generated)
{
  "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 using enrich-incident don'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.

Keep custom nodes single-purpose. A node that "does everything" is hard to test and debug. Build small focused nodes and compose them in the workflow canvas using the visual editor.