Skip to main content
Version: Latest (4.6.0)

Workflow Automation Flow Actions Guide

Action types, flow logic, and patterns for ServiceNow WFA flows. Covers record operations, communication actions, approvals, tasks, attachments, control flow, and complete flow patterns.

Actions

Table Actions

action.core.createRecord

InputTypeRequiredDescription
table_namestringYesTarget table
valuesTemplateValueYesField values

Outputs: record (reference), table_name (string)

const incident = wfa.action(action.core.createRecord,
{ $id: Now.ID["create_incident"] },
{
table_name: "incident",
values: TemplateValue({
short_description: wfa.dataPill(_params.trigger.subject, "string"),
priority: "1",
caller_id: wfa.dataPill(_params.trigger.user, "reference")
})
}
);

action.core.updateRecord

InputTypeRequiredDescription
table_namestringYesTarget table
recordreferenceYesRecord sys_id to update
valuesTemplateValueYesFields to update

Outputs: record (reference), table_name (string)

wfa.action(action.core.updateRecord,
{ $id: Now.ID["update"] },
{
table_name: "incident",
record: wfa.dataPill(_params.trigger.current, "reference"),
values: TemplateValue({
assignment_group: wfa.dataPill(group.Record, "reference"),
state: 2
})
}
);

action.core.deleteRecord

InputTypeRequiredDescription
recordreferenceYesRecord sys_id to delete

No output fields. Inside forEach loops, wrap the record reference:

wfa.action(action.core.deleteRecord, { $id: Now.ID["delete"] }, {
record: `${wfa.dataPill(record, "reference")}`
});

action.core.lookUpRecord

Looks up a single record. Returns the first match.

InputTypeRequiredDescription
tablestringYesTable to search
conditionsstringYesEncoded query
sort_columnstringNoSort field
sort_typechoiceNo'sort_asc' or 'sort_desc'

Outputs: Record (reference, uppercase), status (choice: '0'=success, '1'=error), error_message (string)

action.core.lookUpRecords

Looks up multiple records. Returns array and count.

InputTypeRequiredDescription
tablestringYesTable to search
conditionsstringYesEncoded query
max_resultsintegerNoMax records (default 1000)

Outputs: Records (array, uppercase), Count (integer, uppercase)

const results = wfa.action(action.core.lookUpRecords,
{ $id: Now.ID["find"] },
{ table: "incident", conditions: "active=true^priority=1", max_results: 50 }
);

wfa.flowLogic.forEach(
wfa.dataPill(results.Records, "array.object"),
{ $id: Now.ID["loop"] },
(item) => { /* process each */ }
);

Important: lookUpRecord and lookUpRecords output field names are uppercase (Record, Records, Count) unlike other actions.

action.core.updateMultipleRecords

Bulk updates all matching records.

InputTypeRequiredDescription
table_namestringYesTarget table
conditionsstringYesEncoded query filter
field_valuesTemplateValueYesFields to update (note: field_values, not values)

Outputs: count (integer), status (choice)

action.core.createOrUpdateRecord

Upserts based on unique fields defined in the table dictionary.

InputTypeRequiredDescription
table_namestringYesTarget table
fieldsTemplateValueYesFields including unique identifiers

Outputs: record (reference), status (choice: 'created', 'updated', 'error')

Communication Actions

action.core.sendEmail

InputTypeRequiredDescription
ah_tostringYesComma-separated recipients
ah_subjectstringYesSubject line (supports data pills)
ah_bodyhtmlNoBody (static strings only -- data pills NOT supported)
recordreferenceNoRelated record for tracking
table_namestringNoTable of related record
ah_ccstringNoCC recipients
ah_bccstringNoBCC recipients

Outputs: email (reference to sys_email)

Important: ah_body only accepts static strings. For dynamic email content, use action.core.sendNotification with a notification template.

action.core.sendNotification

InputTypeRequiredDescription
notificationreferenceYesNotification template (sys_id or name)
recordreferenceNoAssociated record
table_namestringNoTable of associated record

No output fields.

action.core.sendSms

InputTypeRequiredDescription
recipientsstringYesPhone number (E.164 format recommended)
messagestringYesSMS content (plain text)

Important: recipients must use template literal wrapping when using data pills:

recipients: `${wfa.dataPill(user.phone, "string")}`

action.core.associateRecordToEmail

Associates a record with a sys_email record (updates the Target field).

InputTypeRequiredDescription
target_recordreferenceYesRecord to associate
email_recordreferenceYesEmail record from sys_email

action.core.getEmailHeader

Retrieves a specific email header value.

InputTypeRequiredDescription
target_headerstringYesHeader name (case-insensitive)
email_recordreferenceYessys_email record

Outputs: header_value (string)

action.core.getLatestResponseTextFromEmail

Extracts the latest reply text from an email thread, stripping quoted prior messages.

InputTypeRequiredDescription
email_recordreferenceYessys_email record

Outputs: latest_response_text (string)

Control Actions

action.core.log

InputTypeRequiredDescription
log_levelstringYes"info", "warn", or "error"
log_messagestringYesMessage (max 255 chars, supports data pills in template literals)

action.core.fireEvent

Fires a registered ServiceNow system event.

InputTypeRequiredDescription
event_namereferenceYesEvent name string (e.g., 'incident.assigned')
recordreferenceYesAssociated record (template literal wrapping required)
tablestringNoTable name
parm1stringNoFirst parameter (template literal wrapping required)
parm2stringNoSecond parameter

No output fields. Fire-and-forget -- event handlers run asynchronously.

action.core.waitForCondition

Pauses flow until a record matches a condition. Supports optional timeout.

InputTypeRequiredDescription
recordreferenceYesRecord to monitor (template literal wrapping required)
conditionsstringYesEncoded query condition
table_namestringNoTable name
timeout_flagbooleanNoEnable timeout
timeout_durationDurationNoHow long to wait
timeout_schedulereferenceNoBusiness hours schedule (cmn_schedule)

Outputs: state (choice: '0'=condition met, '1'=timeout)

action.core.waitForEmailReply

Pauses flow until a reply arrives on a sent email.

InputTypeRequiredDescription
recordreferenceYesOutgoing sys_email record from sendEmail output
enable_timeoutbooleanNoEnable timeout
timeout_durationDurationNoHow long to wait

Outputs: state (choice: '0'=reply received, '1'=timeout), email_reply (reference, empty on timeout)

action.core.waitForMessage

Pauses flow until an external message is received via the Flow API.

InputTypeRequiredDescription
messagestringYesExact message string that resumes flow (case-sensitive)
enable_timeoutbooleanNoEnable timeout
timeoutDurationNoDuration to wait (note: timeout, not timeout_duration)

Outputs: payload (string, empty on timeout)

Approval Actions

action.core.askForApproval

InputTypeRequiredDescription
recordreferenceYesRecord requiring approval
tablestringNoTable name
approval_conditionsobjectYesApproval rules (use wfa.approvalRules())
approval_reasonstringNoReason for approval (max 160 chars)
approval_fieldstringNoField to store approval state
journal_fieldstringNoField to store approval history
due_datedatetimeNoDue date (use wfa.approvalDueDate())

Outputs: approval_state (choice: approved, rejected, requested, not_required, cancelled)

wfa.approvalRules()

wfa.approvalRules({
conditionType: "OR",
ruleSets: [{
action: "Approves", // 'Approves' | 'Rejects' | 'ApprovesRejects'
conditionType: "AND",
rules: [[{
ruleType: "Any", // 'Any' | 'All' | 'Count' | 'Percent'
users: ["user_sys_id"],
groups: [],
manual: false
}]]
}]
})

ruleType values:

  • 'Any' -- approved when ANY single approver approves
  • 'All' -- approved when ALL approvers approve
  • 'Count' -- specific number of approvals required (set count)
  • 'Percent' -- percentage of approvals required (set percent)

wfa.approvalDueDate()

wfa.approvalDueDate({
action: "reject", // 'none' | 'approve' | 'reject' | 'cancel'
dateType: "actual", // 'actual' | 'relative'
date: "{}", // '{}' for actual, datapill for relative
duration: 5,
durationType: "days", // 'years' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes'
daysSchedule: "" // Schedule sys_id for business hours, '' for calendar days
})

Task Actions

action.core.createTask

InputTypeRequiredDescription
task_tablestringYesTask table name (task, sc_task, change_task, etc.)
field_valuesTemplateValueNoField values (note: field_values, not values)
waitbooleanNoWait for task completion (default false)

Outputs: Record (reference, uppercase), Table (string)

Common task tables: task, sc_task, sysapproval_approver, change_task, incident_task, problem_task

Service Catalog Actions

action.core.submitCatalogItemRequest

InputTypeRequiredDescription
catalog_itemreferenceYessys_id of catalog item
catalog_item_inputsstringNoVariables in ^-delimited format
sysparm_quantityintegerNoQuantity (default 1)
sysparm_requested_forreferenceNoUser to request for

Outputs: requested_item (reference to sc_req_item), status (choice: 0=success, 1=error, 2=timeout)

action.core.getCatalogVariables

Retrieves variable values from a catalog request item. Used with trigger.application.serviceCatalog.

InputTypeRequiredDescription
requested_itemreferenceYesRequest item reference
template_catalog_itemstringYesCatalog item reference as template literal
catalog_variablesarrayYesArray of variable references

Outputs: Named by variable name (e.g., catalogVars.memory, catalogVars.storage)

action.core.createCatalogTask

InputTypeRequiredDescription
ah_requested_itemreferenceYesRequest item reference
ah_short_descriptionstringYesTask description
ah_fieldsTemplateValueNoAdditional field values

SLA Actions

action.core.slaPercentageTimer

Pauses flow until a specified percentage of SLA time has elapsed.

InputTypeRequiredDescription
percentageintegerYesSLA percentage to wait for (0-100)

Outputs: status (choice: completed, paused, repair, skipped, cancelled), scheduled_end_date_time (datetime)

Attachment Actions

All attachment actions follow the same wfa.action(action.core.<name>, { $id }, { ... }) pattern.

ActionKey InputsKey OutputsUse Case
getAttachmentsOnRecordsource_record (template literal)parameter (records list), parameter1 (count)List attachments
copyAttachmentsource_record, target_recordsys_attachment (reference)Copy attachment (original preserved)
moveAttachmentsource_record, target_recordsys_attachment (reference)Move attachment (removed from source)
deleteAttachmentsource_recordnoneDelete attachment(s)
lookupAttachmentsource_record, file_namesys_id, file_namesFind attachment by name
lookUpEmailAttachmentsemail_recordparameter (records), parameter1 (count)Get email attachments
moveEmailAttachmentsToRecordemail_record, target_recordnoneMove all email attachments

Important: source_record and target_record inputs require template literal wrapping:

source_record: `${wfa.dataPill(_params.trigger.current, "reference")}`

Flow Logic

Conditional: if / elseIf / else

wfa.flowLogic.if(
{
$id: Now.ID["check"],
condition: `${wfa.dataPill(_params.trigger.current.priority, "string")}=1`
},
() => { /* actions for priority 1 */ }
);

wfa.flowLogic.elseIf(
{
$id: Now.ID["check_p2"],
condition: `${wfa.dataPill(_params.trigger.current.priority, "string")}=2`
},
() => { /* actions for priority 2 */ }
);

wfa.flowLogic.else(
{ $id: Now.ID["default"] },
() => { /* default actions */ }
);

Loops: forEach

wfa.flowLogic.forEach(
wfa.dataPill(results.Records, "array.object"),
{ $id: Now.ID["loop"], annotation: "Process each record" },
(item) => {
// Use wfa.dataPill(item.field, "type") to access fields
}
);

Loop Control: exitLoop / skipIteration

// Exit loop early (break)
wfa.flowLogic.exitLoop({ $id: Now.ID["exit"] });

// Skip current iteration (continue)
wfa.flowLogic.skipIteration({ $id: Now.ID["skip"] });

Flow Termination: endFlow

wfa.flowLogic.endFlow({ $id: Now.ID["end"] });

Condition Syntax Reference

OperatorExampleDescription
=priority=1Equals
!=state!=6Not equals
<, <=, >, >=priority<=2Comparison
ISEMPTYassigned_toISEMPTYField is empty
ISNOTEMPTYassignment_groupISNOTEMPTYField has value
INstateIN1,2,3In list
STARTSWITHnumberSTARTSWITHINCStarts with
CONTAINSshort_descriptionCONTAINSfailedContains
^priority=1^active=trueAND
^ORpriority=1^ORpriority=2OR

Flow Patterns

Simple Automation

Structure: Trigger followed by Single Action

import { Flow, wfa, trigger, action } from "@servicenow/sdk/automation";

Flow(
{
$id: Now.ID["auto_assign_high_priority"],
name: "Auto-Assign High Priority",
description: "Assigns critical incidents to on-call engineer"
},
wfa.trigger(trigger.record.created, { $id: Now.ID["incident_created"] }, {
table: "incident",
condition: "priority<=2^active=true",
run_flow_in: "background"
}),
_params => {
wfa.action(action.core.updateRecord, { $id: Now.ID["assign"] }, {
table_name: "incident",
record: wfa.dataPill(_params.trigger.current, "reference"),
values: TemplateValue({
assigned_to: "<oncall_engineer_sys_id>",
assignment_group: "<it_support_group_sys_id>",
state: 2
})
});
}
);

Conditional Routing

Structure: Trigger followed by if/elseIf/else with different actions per branch

import { Flow, wfa, trigger, action } from "@servicenow/sdk/automation";

Flow(
{
$id: Now.ID["priority_routing"],
name: "Priority-Based Routing",
description: "Routes incidents to teams based on priority"
},
wfa.trigger(trigger.record.created, { $id: Now.ID["trigger"] }, {
table: "incident", condition: "active=true", run_flow_in: "background"
}),
_params => {
wfa.flowLogic.if(
{ $id: Now.ID["check_p1"],
condition: `${wfa.dataPill(_params.trigger.current.priority, "string")}=1` },
() => {
wfa.action(action.core.updateRecord, { $id: Now.ID["assign_p1"] }, {
table_name: "incident",
record: wfa.dataPill(_params.trigger.current, "reference"),
values: TemplateValue({ assignment_group: "<senior_team_sys_id>", state: 2 })
});
wfa.action(action.core.sendEmail, { $id: Now.ID["notify_p1"] }, {
ah_to: "senior-team@company.com",
ah_subject: `CRITICAL: ${wfa.dataPill(_params.trigger.current.number, "string")}`,
ah_body: "Critical incident requires immediate attention."
});
}
);
wfa.flowLogic.elseIf(
{ $id: Now.ID["check_p2"],
condition: `${wfa.dataPill(_params.trigger.current.priority, "string")}=2` },
() => {
wfa.action(action.core.updateRecord, { $id: Now.ID["assign_p2"] }, {
table_name: "incident",
record: wfa.dataPill(_params.trigger.current, "reference"),
values: TemplateValue({ assignment_group: "<regular_team_sys_id>", state: 2 })
});
}
);
wfa.flowLogic.else({ $id: Now.ID["default"] }, () => {
wfa.action(action.core.updateRecord, { $id: Now.ID["assign_general"] }, {
table_name: "incident",
record: wfa.dataPill(_params.trigger.current, "reference"),
values: TemplateValue({ assignment_group: "<general_queue_sys_id>" })
});
});
}
);

Record Iteration

Structure: Trigger followed by lookUpRecords followed by forEach with action per record

import { Flow, wfa, trigger, action } from "@servicenow/sdk/automation";

Flow(
{
$id: Now.ID["bulk_assign"],
name: "Bulk Assign Unassigned Incidents"
},
wfa.trigger(trigger.scheduled.daily, { $id: Now.ID["trigger"] }, {
time: Time({ hours: 8, minutes: 0, seconds: 0 }, "UTC")
}),
_params => {
const incidents = wfa.action(action.core.lookUpRecords,
{ $id: Now.ID["find"] },
{ table: "incident", conditions: "active=true^assigned_toISEMPTY^priority<=2", max_results: 50 }
);

wfa.flowLogic.if(
{ $id: Now.ID["check"], condition: `${wfa.dataPill(incidents.Count, "integer")}>0` },
() => {
wfa.flowLogic.forEach(
wfa.dataPill(incidents.Records, "array.object"),
{ $id: Now.ID["loop"] },
(incident) => {
wfa.action(action.core.updateRecord, { $id: Now.ID["assign"] }, {
table_name: "incident",
record: wfa.dataPill(incident, "reference"),
values: TemplateValue({ assignment_group: "<support_group_sys_id>" })
});
}
);
}
);
}
);

Approval Workflow

Structure: Trigger followed by askForApproval followed by route by approval result

import { Flow, wfa, trigger, action } from "@servicenow/sdk/automation";

Flow(
{
$id: Now.ID["change_approval"],
name: "Change Request Approval Workflow"
},
wfa.trigger(trigger.record.created, { $id: Now.ID["trigger"] }, {
table: "change_request", condition: "type=normal"
}),
_params => {
const approval = wfa.action(action.core.askForApproval,
{ $id: Now.ID["cab_approval"] },
{
record: wfa.dataPill(_params.trigger.current, "reference"),
table: "change_request",
approval_reason: "CAB approval required",
approval_conditions: wfa.approvalRules({
conditionType: "OR",
ruleSets: [{
action: "ApprovesRejects",
conditionType: "AND",
rules: [[{
ruleType: "Percent", percent: 50,
users: [], groups: ["<cab_group_sys_id>"], manual: false
}]]
}]
}),
due_date: wfa.approvalDueDate({
action: "reject", dateType: "actual", date: "{}",
duration: 5, durationType: "days", daysSchedule: ""
})
}
);

wfa.flowLogic.if(
{ $id: Now.ID["approved"],
condition: `${wfa.dataPill(approval.approval_state, "choice")}=approved` },
() => {
wfa.action(action.core.updateRecord, { $id: Now.ID["approve"] }, {
table_name: "change_request",
record: wfa.dataPill(_params.trigger.current, "reference"),
values: TemplateValue({ state: 3, comments: "CAB approved" })
});
}
);
wfa.flowLogic.elseIf(
{ $id: Now.ID["rejected"],
condition: `${wfa.dataPill(approval.approval_state, "choice")}=rejected` },
() => {
wfa.action(action.core.updateRecord, { $id: Now.ID["reject"] }, {
table_name: "change_request",
record: wfa.dataPill(_params.trigger.current, "reference"),
values: TemplateValue({ state: 4, comments: "CAB rejected" })
});
}
);
}
);

Monitoring Pattern

Structure: Scheduled Trigger followed by lookUpRecords followed by forEach with action per record

Use when requirements include "while", "monitor", "periodically", "every day/hour", or ongoing checks. Differs from record iteration by using a scheduled trigger instead of an event trigger.

import { Flow, wfa, trigger, action } from "@servicenow/sdk/automation";

Flow(
{
$id: Now.ID["health_check"],
name: "Health Check Every 15 Minutes"
},
wfa.trigger(trigger.scheduled.repeat, { $id: Now.ID["trigger"] }, {
repeat: Duration({ minutes: 15 })
}),
_params => {
const unassigned = wfa.action(action.core.lookUpRecords,
{ $id: Now.ID["check"] },
{ table: "incident", conditions: "assigned_toISEMPTY^priority=1^active=true", max_results: 100 }
);

wfa.flowLogic.if(
{ $id: Now.ID["threshold"],
condition: `${wfa.dataPill(unassigned.Count, "integer")}>10` },
() => {
wfa.action(action.core.sendEmail, { $id: Now.ID["alert"] }, {
ah_to: "admin@company.com",
ah_subject: "Alert: High number of unassigned P1 incidents",
ah_body: "There are more than 10 unassigned P1 incidents. Please investigate."
});
}
);
}
);

Service Catalog Fulfillment

Structure: serviceCatalog trigger followed by getCatalogVariables followed by conditional routing and createCatalogTask

Use trigger.application.serviceCatalog when automating service catalog request fulfillment. It provides direct access to request item context.

import { Flow, wfa, trigger, action } from "@servicenow/sdk/automation";

Flow(
{
$id: Now.ID["laptop_fulfillment"],
name: "Laptop Fulfillment Workflow"
},
wfa.trigger(trigger.application.serviceCatalog,
{ $id: Now.ID["catalog_trigger"] },
{ run_flow_in: "background" }
),
_params => {
wfa.action(action.core.createCatalogTask, { $id: Now.ID["create_task"] }, {
ah_requested_item: wfa.dataPill(_params.trigger.request_item, "reference"),
ah_short_description: "Fulfill laptop request",
ah_fields: TemplateValue({
assignment_group: "Hardware Fulfillment Team",
priority: "3"
})
});
}
);

Complete Flow Example

A full example combining configuration, trigger, actions, conditions, and data pills:

import { Flow, wfa, trigger, action } from "@servicenow/sdk/automation";

Flow(
// 1. Configuration
{
$id: Now.ID["escalate_high_priority"],
name: "Escalate High Priority Incidents",
description: "Escalates P1 incidents to senior team with notification",
runAs: "system",
flowPriority: "HIGH"
},

// 2. Trigger
wfa.trigger(trigger.record.created,
{ $id: Now.ID["incident_created"] },
{
table: "incident",
condition: "priority=1^active=true",
run_flow_in: "background"
}
),

// 3. Flow Logic
_params => {
// Update record
wfa.action(action.core.updateRecord, { $id: Now.ID["assign"] }, {
table_name: "incident",
record: wfa.dataPill(_params.trigger.current, "reference"),
values: TemplateValue({
assignment_group: "<senior_team_sys_id>",
state: 2
})
});

// Send notification
wfa.action(action.core.sendEmail, { $id: Now.ID["notify"] }, {
ah_to: "senior-team@company.com",
ah_subject: `Critical: ${wfa.dataPill(_params.trigger.current.number, "string")}`,
ah_body: "A critical incident requires immediate attention."
});

// Log action
wfa.action(action.core.log, { $id: Now.ID["log"] }, {
log_level: "info",
log_message: `Escalated incident ${wfa.dataPill(_params.trigger.current.number, "string")}`
});
}
);

Important Notes

  • All flows must be created under the fluent/flows folder
  • Every flow requires exactly one trigger
  • Background execution (run_flow_in: 'background') is recommended for most flows
  • TemplateValue, Time, and Duration are available globally -- do not import
  • In conditions, always use template literals: `${wfa.dataPill(...)}=value`
  • Conditions use encoded query format: use = not ==, ^ for AND, ^OR for OR