Skip to main content
Version: 4.6.0

Workflow Automation Custom Action Guide

Guide for creating reusable custom actions using the Fluent SDK. Custom actions encapsulate a sequence of OOB steps with typed inputs and outputs, and can be invoked from any Flow or Subflow via wfa.action().

When to Use

  • Encapsulating a reusable sequence of OOB steps shared across multiple flows
  • Standardizing common operations (e.g., incident escalation, user provisioning)
  • Creating domain-specific action libraries for your application
  • Reducing duplication when the same multi-step logic appears in several flows

Important: Custom actions contain only wfa.actionStep() calls — no flow logic (if/else, forEach), no triggers, and no nested custom actions.


Core Principles

  1. Single Responsibility: Each custom action should do one thing well. Use descriptive verb-phrase names (e.g., "Escalate Incident", "Provision User Account").

  2. OOB Steps Only: The body contains only wfa.actionStep() calls. No wfa.flowLogic.*, no wfa.action(), no nested Action().

  3. Sequential Execution: Steps execute in order. Capture each step's return value to pass outputs to downstream steps via wfa.dataPill().

  4. File Location: Place custom actions in the fluent/actions/ directory.

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


Critical Syntax Requirements

MANDATORY patterns -- violations cause runtime errors:

  1. actionStep namespace: Use actionStep.createRecord not action.core.createRecord. The actionStep.* namespace is for custom action bodies; action.core.* is for flows.

  2. TemplateValue: Use TemplateValue({ }) not wfa.TemplateValue({ }). It is available globally -- do not import it.

  3. Step output chaining: Capture wfa.actionStep() return value as const to reference outputs in downstream steps.

  4. Step parameter prefixes: Some steps use prefixed parameters (e.g., create_record_table_name, create_record_field_values for createRecord; log_message, log_level for log).

  5. Error handling: Use errorHandlingType: 'dont_stop_the_action' to continue on error, or 'stop_the_action' (default) to halt.


Action Constructor

import { Action, wfa, actionStep } from "@servicenow/sdk/automation";
import { StringColumn, BooleanColumn, ReferenceColumn } from "@servicenow/sdk/core";

export const myAction = Action(
config, // Action metadata, inputs, outputs
body // Sequential wfa.actionStep() calls
);

Action Configuration Properties

PropertyTypeRequiredDefaultDescription
$idstringYes-Unique identifier (Now.ID['name'])
namestringYes-Display name shown in Flow Designer
descriptionstringNo-Action purpose and behavior
categorystringNo"default"Grouping category
access'public' | 'private'No'public'Visibility scope
inputsRecord<string, Column>No{}Input parameter definitions
outputsRecord<string, Column>No{}Output parameter definitions

Column Types

Import from @servicenow/sdk/core:

TypeDescriptionExample
StringColumnText valuesStringColumn({ label: 'Name', mandatory: true })
IntegerColumnWhole numbersIntegerColumn({ label: 'Count' })
BooleanColumnTrue/falseBooleanColumn({ label: 'Success' })
ReferenceColumnTable record referenceReferenceColumn({ label: 'User', referenceTable: 'sys_user' })
DecimalColumnDecimal numbers (fixed precision)DecimalColumn({ label: 'Amount' })
FloatColumnFloating-point numbersFloatColumn({ label: 'Score' })
DateTimeColumnDate and timeDateTimeColumn({ label: 'Due Date' })

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

TypeDescription
FlowObjectNested object with typed fields
FlowArrayArray of typed elements

wfa.actionStep()

Embeds an OOB step inside a custom action body. Returns typed outputs for downstream steps.

const result = wfa.actionStep(
actionStep.<stepName>,
{ $id: Now.ID['step_id'], label: 'Step Label' },
{ /* step-specific parameters */ }
);

Available OOB Steps

ServiceNow Data

StepDescription
actionStep.createRecordCreates a record. Params: create_record_table_name, create_record_field_values (TemplateValue).
actionStep.updateRecordUpdates a record. Params: table, record, values (TemplateValue).
actionStep.deleteRecordDeletes a record. Params: table, record.
actionStep.deleteMultipleRecordsDeletes multiple records. Params: table_name, conditions.
actionStep.lookUpRecordLooks up a single record. Params: table_name, conditions.
actionStep.lookUpRecordsLooks up multiple records. Params: table_name, conditions.
actionStep.createOrUpdateRecordUpserts a record. Params: table, fields (TemplateValue).
actionStep.createTaskCreates a task with optional wait. Params: create_record_table_name, create_record_field_values, wait.
actionStep.createRecordForRemoteTableCreates a record on a remote table. Params: query_id, create_record_table_name, create_record_field_values.
actionStep.updateMultipleRecordsUpdates multiple records. Params: table_name, conditions, field_values.
actionStep.fireEventFires a system event. Params: event_name, record, table, parm1, parm2.
actionStep.waitForConditionPauses until condition met. Params: record, conditions, timeout_flag, timeout_duration.
actionStep.waitForEmailReplyPauses until email reply. Params: record, enable_timeout, timeout_duration.
actionStep.waitForMessagePauses until message received. Params: message, enable_timeout, timeout.
actionStep.askForApprovalRequests approval. Params: table, record, approval_conditions, approval_reason, due_date.

Utilities

StepDescription
actionStep.logLogs a message. Params: log_message, log_level ("info", "warn", "error").
actionStep.emailSends an email. Params: ah_to, ah_subject, ah_body.
actionStep.smsSends an SMS. Params: recipients, message.
actionStep.notificationSends a notification. Params: notification, record.
actionStep.scriptExecutes server-side script.
actionStep.collectActivityContextCollects activity context data.
actionStep.createAppFromPayloadCreates an application from a payload.
actionStep.getLatestResponseTextFromEmailExtracts latest reply from email thread. Params: email_record.

Anti-Patterns

No Flow Logic Inside Actions

Custom actions are strictly sequential. Do not use wfa.flowLogic.* constructs.

// WRONG - Flow logic in custom action
(params) => {
wfa.flowLogic.if({ ... }, () => { ... }); // NOT allowed!
};

// CORRECT - Sequential steps only
(params) => {
wfa.actionStep(actionStep.createRecord, { ... }, { ... });
wfa.actionStep(actionStep.log, { ... }, { ... });
};

No Nested Custom Actions

Custom actions cannot call other custom actions. Use only OOB steps from actionStep.*.

// WRONG - Calling another custom action
(params) => {
wfa.action(otherCustomAction, { ... }, { ... }); // NOT allowed!
};

Data Pill Usage

Same rules as flows -- do not assign data pills to variables. Use directly in step parameters.

// WRONG
const id = wfa.dataPill(params.inputs.recordId, 'string');
wfa.actionStep(actionStep.lookUpRecord, { ... }, { conditions: `sys_id=${id}` });

// CORRECT
wfa.actionStep(actionStep.lookUpRecord, { ... }, {
conditions: `sys_id=${wfa.dataPill(params.inputs.recordId, 'string')}`
});

Patterns

Basic Custom Action

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

export const logAndCreate = Action(
{
$id: Now.ID["log_and_create"],
name: "Log and Create Incident",
description: "Logs a message and creates an incident",
inputs: {
description: StringColumn({ label: "Description", mandatory: true }),
priority: StringColumn({ label: "Priority", mandatory: true }),
},
outputs: {
success: BooleanColumn({ label: "Success" }),
},
},
(params) => {
wfa.actionStep(
actionStep.log,
{ $id: Now.ID["log_start"], label: "Log start" },
{
log_message: `Creating incident: ${wfa.dataPill(params.inputs.description, "string")}`,
log_level: "info",
}
);

wfa.actionStep(
actionStep.createRecord,
{ $id: Now.ID["create_incident"], label: "Create incident" },
{
create_record_table_name: "incident",
create_record_field_values: TemplateValue({
short_description: wfa.dataPill(params.inputs.description, "string"),
priority: wfa.dataPill(params.inputs.priority, "string"),
}),
}
);
}
);

Chaining Step Outputs

import { Action, wfa, actionStep } from "@servicenow/sdk/automation";
import { ReferenceColumn, StringColumn } from "@servicenow/sdk/core";

export const createAndNotify = Action(
{
$id: Now.ID["create_and_notify"],
name: "Create Record and Notify",
inputs: {
assignee: ReferenceColumn({ label: "Assignee", referenceTable: "sys_user", mandatory: true }),
description: StringColumn({ label: "Description", mandatory: true }),
},
outputs: {},
},
(params) => {
const created = wfa.actionStep(
actionStep.createRecord,
{ $id: Now.ID["create_step"], label: "Create incident" },
{
create_record_table_name: "incident",
create_record_field_values: TemplateValue({
short_description: wfa.dataPill(params.inputs.description, "string"),
assigned_to: wfa.dataPill(params.inputs.assignee, "reference"),
}),
}
);

wfa.actionStep(
actionStep.log,
{ $id: Now.ID["log_result"], label: "Log result" },
{
log_message: wfa.dataPill(created.record.number, "string"),
log_level: "info",
}
);
}
);

Using a Custom Action in a Flow

import { Flow, wfa, trigger } from "@servicenow/sdk/automation";
import { logAndCreate } from "../actions/log-and-create.now";

Flow(
{
$id: Now.ID["auto_create_flow"],
name: "Auto Create Incident from Email",
},
wfa.trigger(trigger.application.inboundEmail,
{ $id: Now.ID["email_trigger"] },
{ email_conditions: "subjectLIKEurgent" }
),
params => {
wfa.action(
logAndCreate,
{ $id: Now.ID["invoke_action"] },
{
description: wfa.dataPill(params.trigger.subject, "string"),
priority: "1",
}
);
}
);

Important Notes

  • Custom actions must be placed in the fluent/actions/ directory
  • Always export as export const for use in flows
  • Only wfa.actionStep() calls are allowed in the body -- no flow logic, no nested actions
  • TemplateValue is available globally -- do not import
  • Step outputs are captured by assigning wfa.actionStep() return value to a const
  • Use wfa.dataPill() to pass action inputs or step outputs as dynamic values