Skip to main content
Version: 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