Skip to main content
Version: 4.6.0

Workflow Automation Subflow Guide

Guide for creating and invoking reusable Subflows using the Fluent SDK. Subflows encapsulate flow logic with typed inputs and outputs, and can be invoked from any Flow or another Subflow via wfa.subflow().

When to Use

  • Same logic is needed across multiple flows
  • Breaking a complex flow into composable, testable pieces
  • A flow delegates a discrete unit of work that returns outputs
  • Standardizing reusable logic (e.g., user validation, record enrichment)

Flow vs Subflow:

FlowSubflow
Has triggerYes (exactly one)No
Can be invokedNo (event-driven only)Yes, from flows or other subflows
Has inputs/outputsNo typed inputs/outputsYes, typed column-based
File locationfluent/flows/fluent/flows/

Core Principles

  1. No Trigger: Subflows do not have triggers. They are invoked explicitly via wfa.subflow().

  2. Typed Inputs and Outputs: Define inputs and outputs using column types (StringColumn, BooleanColumn, etc.) for type-safe invocation.

  3. Set Outputs Explicitly: Use wfa.flowLogic.assignSubflowOutputs() inside the body to return data to the caller. This is the only way to set outputs.

  4. Export Required: Always export the subflow as export const so it can be imported by flows.

  5. Body is Optional: Subflow(config) with no body is valid for stub definitions or cross-file references.

  6. Full Flow Logic: Unlike custom actions, subflows support all wfa.flowLogic.* constructs (if/else, forEach, exitLoop, etc.) and wfa.action() calls.


Subflow Constructor

import { Subflow, wfa, action } from "@servicenow/sdk/automation";
import { StringColumn, BooleanColumn } from "@servicenow/sdk/core";

export const mySubflow = Subflow(
config, // Subflow metadata, inputs, outputs, flowVariables
body // Optional function with flow logic
);

Subflow Configuration Properties

PropertyTypeRequiredDefaultDescription
$idstringYes-Unique identifier (Now.ID['name'])
namestringYes-Display name shown in Flow Designer
descriptionstringNo-Subflow purpose and behavior
runAs'system' | 'user'No'user'Execution security context
runWithRolesstring[]No[]Role sys_ids for elevated permissions
flowPriority'LOW' | 'MEDIUM' | 'HIGH'No'MEDIUM'Execution queue priority
access'public' | 'package_private'No'public'Visibility scope
categorystringNo-Grouping category in Flow Designer
inputsRecord<string, Column>No{}Input schema passed by the caller
outputsRecord<string, Column>No{}Output schema set via assignSubflowOutputs
flowVariablesRecord<string, Column>No{}Internal variables scoped to this execution

Column Types

Import from @servicenow/sdk/core:

TypeDescription
StringColumnText values
IntegerColumnWhole numbers
BooleanColumnTrue/false values
ReferenceColumnReference to a table record (use referenceTable option)
DecimalColumnDecimal numbers (fixed precision)
FloatColumnFloating-point numbers
DateTimeColumnDate and time values

Import from @servicenow/sdk/automation for complex types:

TypeDescription
FlowObjectNested object with typed fields
FlowArrayArray of typed elements

Invoking a Subflow

Use wfa.subflow() inside a Flow or another Subflow.

import { mySubflow } from "./my-subflow.now";

const result = wfa.subflow(
mySubflow,
{ $id: Now.ID["instance_id"], annotation: "Description" },
{
inputField: wfa.dataPill(someValue, "string"),
waitForCompletion: true,
}
);

// Access outputs via data pills
wfa.dataPill(result.outputField, "string");
ParameterTypeRequiredDescription
subflowSubflowYesExported subflow constant
$idstringYesUnique identifier for this invocation
annotationstringNoDescription of this invocation
inputsobjectYesInput values matching subflow's input schema
waitForCompletionbooleanNoIf true, caller waits for subflow to finish (default false)

Important: Set waitForCompletion: true when downstream logic depends on the subflow's outputs.


Setting Outputs

Use wfa.flowLogic.assignSubflowOutputs() inside the subflow body. Always pass params.outputs as the second argument.

wfa.flowLogic.assignSubflowOutputs(
{ $id: Now.ID["set_outputs"], annotation: "Return results" },
params.outputs,
{ success: true, record: wfa.dataPill(lookup.Record, "reference") }
);
ParameterDescription
FirstMetadata with $id and optional annotation
SecondAlways params.outputs -- do not construct a custom object
ThirdKey/value pairs to assign; values can be literals or data pills

Anti-Patterns

Do NOT Forget assignSubflowOutputs

If your subflow declares outputs, you must call assignSubflowOutputs in the body. Without it, the caller receives undefined values.

Data Pill Rules Apply

Same rules as flows -- do not assign data pills to const variables in the subflow body. Use them directly in action parameters.

Do NOT Add a Trigger

Subflows are invoked, not triggered. Adding trigger-like logic is incorrect.


Patterns

Basic Subflow

import { Subflow, wfa, action } from "@servicenow/sdk/automation";
import { StringColumn, BooleanColumn } from "@servicenow/sdk/core";

export const checkRecordExists = Subflow(
{
$id: Now.ID["check_record_exists"],
name: "Check Record Exists",
runAs: "system",
inputs: {
table: StringColumn({ label: "Table Name", mandatory: true }),
sysId: StringColumn({ label: "Record Sys ID", mandatory: true }),
},
outputs: {
exists: BooleanColumn({ label: "Record Exists", mandatory: true }),
},
},
(params) => {
const lookup = wfa.action(
action.core.lookUpRecord,
{ $id: Now.ID["lookup"] },
{
table: wfa.dataPill(params.inputs.table, "string"),
conditions: `sys_id=${wfa.dataPill(params.inputs.sysId, "string")}`,
}
);

wfa.flowLogic.if(
{
$id: Now.ID["found"],
condition: `${wfa.dataPill(lookup.Record.sys_id, "string")}ISNOTEMPTY`,
},
() => {
wfa.flowLogic.assignSubflowOutputs(
{ $id: Now.ID["set_true"] },
params.outputs,
{ exists: true }
);
}
);

wfa.flowLogic.else({ $id: Now.ID["not_found"] }, () => {
wfa.flowLogic.assignSubflowOutputs(
{ $id: Now.ID["set_false"] },
params.outputs,
{ exists: false }
);
});
}
);

Invoking from a Flow

import { Flow, wfa, trigger, action } from "@servicenow/sdk/automation";
import { checkRecordExists } from "./check-record-exists.now";

Flow(
{
$id: Now.ID["safe_update_flow"],
name: "Safe Update Flow",
runAs: "system",
},
wfa.trigger(trigger.record.updated, { $id: Now.ID["trigger"] }, {
table: "incident",
condition: "active=true",
run_flow_in: "background",
trigger_strategy: "unique_changes",
}),
params => {
const check = wfa.subflow(
checkRecordExists,
{ $id: Now.ID["check_instance"] },
{
table: "sys_user",
sysId: wfa.dataPill(params.trigger.current.assigned_to, "string"),
waitForCompletion: true,
}
);

wfa.flowLogic.if(
{
$id: Now.ID["if_exists"],
condition: `${wfa.dataPill(check.exists, "boolean")}=true`,
},
() => {
wfa.action(action.core.log, { $id: Now.ID["log_ok"] }, {
log_level: "info",
log_message: "Assigned user verified -- proceeding with update",
});
}
);
}
);

Minimal Subflow (No Body)

Valid for stub definitions or when the subflow is defined elsewhere.

import { Subflow } from "@servicenow/sdk/automation";
import { StringColumn, BooleanColumn } from "@servicenow/sdk/core";

export const placeholderSubflow = Subflow({
$id: Now.ID["placeholder"],
name: "Placeholder Subflow",
inputs: {
recordId: StringColumn({ label: "Record ID", mandatory: true }),
},
outputs: {
success: BooleanColumn({ label: "Success" }),
},
});

Important Notes

  • Subflows are placed in the fluent/flows/ directory (same as flows)
  • Always export as export const for use in flows
  • Subflows support full flow logic: wfa.action(), wfa.flowLogic.*, nested wfa.subflow()
  • assignSubflowOutputs is the only way to set output values
  • Set waitForCompletion: true when the caller needs subflow outputs before continuing
  • TemplateValue, Time, and Duration are available globally -- do not import