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
For API signatures, parameter tables, and output fields for every action, see the Action API.
Actions Overview
| Category | Key Actions | Use For |
|---|---|---|
| Record Operations | createRecord, updateRecord, deleteRecord, lookUpRecord, lookUpRecords, updateMultipleRecords, createOrUpdateRecord | CRUD operations |
| Communication | sendEmail, sendNotification, sendSms, associateRecordToEmail, getEmailHeader, getLatestResponseTextFromEmail | Messaging |
| Control | log, fireEvent, waitForCondition, waitForMessage, waitForEmailReply | Flow control / pause |
| Approvals | askForApproval | Approval workflows |
| Task | createTask | Task creation |
| Service Catalog | submitCatalogItemRequest, getCatalogVariables, createCatalogTask | Catalog provisioning |
| SLA | slaPercentageTimer | SLA percentage waits |
| Attachments | getAttachmentsOnRecord, copyAttachment, moveAttachment, moveEmailAttachmentsToRecord, deleteAttachment, lookupAttachment, lookUpEmailAttachments | File handling |
Actions by Operation Type
| Operation | Action | Use When |
|---|---|---|
| Create new record | createRecord | Creating child records, using templates |
| Update existing record | updateRecord | Modifying field values on any record |
| Find one record | lookUpRecord | Single result expected, lookup by key |
| Find multiple records | lookUpRecords | Batch processing, iteration needed |
| Bulk update records | updateMultipleRecords | Mass updates, batch processing |
| Upsert (create or update) | createOrUpdateRecord | Idempotent creation, import workflows |
| Delete a record | deleteRecord | Removing records (typically in forEach) |
| Send email message | sendEmail | Custom email with full template control |
| Send notification template | sendNotification | Using predefined notification templates |
| Send SMS message | sendSms | Text message notifications |
| Request user approval | askForApproval | Single or multi-level approvals |
| Create a task | createTask | Creating work items in task tables |
| Manage attachments | getAttachmentsOnRecord | File operations on records |
| Pause until SLA milestone | slaPercentageTimer | Wait for SLA percentage to be reached |
| Wait until condition met | waitForCondition | Wait until record reaches desired state |
| Wait for external message | waitForMessage | Wait until API sends a resume message |
| Wait for email reply | waitForEmailReply | Wait until a reply arrives on an email |
| Fire a system event | fireEvent | Publish event for downstream handlers |
Common Best Practices
These apply to all actions. Action-specific advice is called out per action below.
- Wrap field values in
TemplateValue({...})-- required forcreateRecord/updateRecord/createTask/updateMultipleRecords/createOrUpdateRecord/createCatalogTask.TemplateValueis global -- don't import it. - Capture outputs as
constto chain into downstream actions:const result = wfa.action(...)thenwfa.dataPill(result.field, "type"). Watch the output field casing (some actions use lowercaserecord/table_name, others use uppercaseRecord/Records/Count/Table-- see the Action API for each). - Use proper data pill types --
'reference'for record fields,'string_full_utf8'for email subject/body,'choice'for choice fields,'records'for record-set outputs used inforEach. - Don't capture data pills in variables in flow bodies (
const x = wfa.dataPill(...)is a footgun). Use the data pill directly inside action parameters. (Exception: inside a custom action body,const step = wfa.actionStep(...)is correct.)
Table Actions
Actions for creating, reading, updating, and deleting records in ServiceNow tables.
For API signatures, parameter tables, and output fields, see the Action API → Table Actions.
Shared considerations
Value-field parameter naming differs per action:
| Action(s) | Value-field parameter |
|---|---|
createRecord, updateRecord | values |
updateMultipleRecords | field_values |
createOrUpdateRecord | fields |
Output field casing differs per action:
| Action | Output field(s) |
|---|---|
createRecord, updateRecord, createOrUpdateRecord | lowercase record |
lookUpRecord | UPPERCASE Record, Table |
lookUpRecords | UPPERCASE Records, Count, Table |
updateMultipleRecords | lowercase status, count, message |
deleteRecord has no outputs.
action.core.createRecord
Creates a new record in any ServiceNow table.
When to Use
- Creating child records from a parent event (e.g., incident from inbound email)
- Creating audit/log records in custom tables
- Template-based creation (duplicate with modifications)
Important Notes
- Missing mandatory fields or invalid references cause the flow to fail -- there is no built-in fail-soft option
Example
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_full_utf8"),
priority: "1",
caller_id: wfa.dataPill(params.trigger.target_record, "reference")
})
}
);
action.core.updateRecord
Updates an existing record in any ServiceNow table.
When to Use
- State/status transitions during a workflow
- Assigning records to users or groups
- Adding work notes or other field updates after a lookup/approval
Best Practices
- Update only changed fields -- including unchanged fields fires unnecessary business rules and engagement messaging
- Beware concurrent updates -- another process may modify the record between your lookup and update; use trigger condition or
lookUpRecordresults as the source of truth
Example
wfa.action(
action.core.updateRecord,
{ $id: Now.ID["assign_incident"] },
{
table_name: "incident",
record: wfa.dataPill(params.trigger.current, "reference"),
values: TemplateValue({
assignment_group: wfa.dataPill(group.Record, "reference"),
state: 2,
work_notes: "Auto-assigned to IT Support team"
})
}
);
action.core.deleteRecord
Permanently deletes a record from any ServiceNow table.
When to Use
- Removing temporary/test/expired records (e.g., scheduled cleanup)
- Cleaning up duplicates after deduplication
- Purging stale integration queue items
Best Practices
- Prefer inactivation (
active: falseviaupdateRecord) over delete when audit trail matters - Inside
forEach, wrap therecordparameter in a template literal:record: `${wfa.dataPill(record, "reference")}` - Wrap in
flowLogic.ifto prevent accidental deletion when conditions aren't fully validated
Important Notes
- Permanent and irreversible -- no rollback. Related records may become orphaned.
Example
// Inside a forEach loop over a record set
wfa.action(
action.core.deleteRecord,
{ $id: Now.ID["delete_record"] },
{
record: `${wfa.dataPill(record, "reference")}`
}
);
action.core.lookUpRecord
Query a single record from any ServiceNow table based on conditions.
When to Use
- Find a user/group by name or email
- Look up reference data before creating/updating records
- Validate record existence before processing
Best Practices
- Always check
status-- verifystatus='0'before using the result;'1'indicates error/not found - Use unique conditions -- match exactly one record (email, number, sys_id); set
if_multiple_records_are_found_actionto'use_first_record'or'error'
Important Notes
- Returns
error_messageon failure (e.g., ACL denial or no match)
Example
const user = wfa.action(
action.core.lookUpRecord,
{ $id: Now.ID["find_user"] },
{
table: "sys_user",
conditions: "email=john.doe@company.com",
if_multiple_records_are_found_action: "use_first_record"
}
);
// Guard with status, then use uppercase Record
wfa.flowLogic.if(
{ $id: Now.ID["found"], condition: `${wfa.dataPill(user.status, "string")}=0` },
() => {
wfa.action(
action.core.updateRecord,
{ $id: Now.ID["deactivate"] },
{
table_name: "sys_user",
record: wfa.dataPill(user.Record, "reference"),
values: TemplateValue({ active: false })
}
);
}
);
action.core.lookUpRecords
Query multiple records from any ServiceNow table based on conditions.
When to Use
- Bulk processing (iterate results with
forEach) - Existence/count checks before creating or updating
- Fetching data sets for aggregation
Best Practices
- Always set
max_results-- prevents timeouts; 100-200 is typical forforEach-driven workflows - Check
CountbeforeforEach-- guards against empty-array iteration
Important Notes
max_resultsdefault is 1000; system max is typically 10,000 (configurable)- Empty result is safe (
Count: 0,Records: [])
Example
const results = wfa.action(
action.core.lookUpRecords,
{ $id: Now.ID["find_p1s"] },
{
table: "incident",
conditions: "active=true^priority=1",
max_results: 100
}
);
wfa.flowLogic.if(
{ $id: Now.ID["has_matches"], condition: `${wfa.dataPill(results.Count, "integer")}>0` },
() => {
wfa.flowLogic.forEach(
wfa.dataPill(results.Records, "records"),
{ $id: Now.ID["each"] },
record => { /* process each record */ }
);
}
);
action.core.updateMultipleRecords
Updates multiple records in a single operation based on query conditions.
When to Use
- Bulk assignment (assign all unassigned records to a group)
- Mass state transitions (close all resolved incidents older than X days)
- Batch inactivation or field cleanup across many records
Best Practices
- Preview with
lookUpRecordsfirst using the same conditions -- confirm what will be updated before running - Business rules fire per record -- expect longer execution and cascade effects for >200 records; for >1000, prefer a scheduled job
Important Notes
statusis'0'(success) or'1'(error);countis records updated;messagecarries error details
Example
const result = wfa.action(
action.core.updateMultipleRecords,
{ $id: Now.ID["bulk_close"] },
{
table_name: "incident",
conditions: "state=6^active=true^sys_updated_on<javascript:gs.daysAgoStart(30)",
field_values: TemplateValue({
state: 7,
active: false,
close_code: "Closed/Resolved by Caller",
close_notes: "Auto-closed after 30 days in resolved state"
})
}
);
action.core.createOrUpdateRecord
Creates a new record if no match is found, or updates the existing record if a match exists (upsert).
When to Use
- External-system data sync (create if new, update if exists)
- User/asset provisioning keyed by unique identifier (email, serial number)
- Idempotent integrations that may run repeatedly
Best Practices
- Include the unique-identifier field in the values -- e.g.,
emailforsys_user,serial_numberforcmdb_ci. Matching uses the table dictionary's unique-field definitions - Check
status-- returns'created','updated', or'error'-- branch on it if create vs. update behavior should differ
Common unique fields by table
| Table | Common unique fields |
|---|---|
sys_user | email, user_name |
sys_user_group, core_company, sc_cat_item, sys_properties | name |
cmdb_ci, cmdb_ci_computer | serial_number, asset_tag |
Example
const user = wfa.action(
action.core.createOrUpdateRecord,
{ $id: Now.ID["upsert_user"] },
{
table_name: "sys_user",
fields: TemplateValue({
email: wfa.dataPill(params.trigger.from_address, "string"),
first_name: "John",
last_name: "Doe",
active: true
})
}
);
// Branch on whether record was created or updated
wfa.flowLogic.if(
{ $id: Now.ID["was_created"], condition: `${wfa.dataPill(user.status, "string")}=created` },
() => { /* handle new user (e.g., send welcome email) */ }
);
Communication Actions
Actions for sending notifications via email, in-platform notifications, and SMS, and for working with sys_email records and headers.
For API signatures, parameter tables, and output fields, see the Action API → Communication Actions.
Choosing the right communication action
sendEmail-- External recipients, rich HTML formatting, off-platform deliverysendNotification-- Internal ServiceNow users, pre-configured templates, in-platform (preferred for internal use)sendSms-- Critical alerts only (per-message cost ~$0.01-0.05; use sparingly)
action.core.sendEmail
Sends rich text emails to addresses, user records, or group records.
When to Use
- External-recipient notifications (customers, vendors)
- Detailed reports/summaries requiring HTML formatting
- Off-platform communication where recipients have no ServiceNow login
Best Practices
ah_bodydoes NOT support data pills -- use static strings only. Data pills work inah_subjectandah_to.- Always set
recordandtable_namefor traceability in the email record's history watermark_email: falsefor external-facing emails (removes the "Sent by ServiceNow" footer)- Keep HTML simple -- basic tags (
<h2>,<p>,<strong>,<ul>,<li>); avoid CSS/JS
Important Notes
- Emails are recorded in
sys_emailand on the linked record's history - Sending many individual emails in a
forEachcan trip spam filters -- aggregate into a single summary when possible
Example
wfa.action(
action.core.sendEmail,
{ $id: Now.ID["notify_user"] },
{
ah_to: wfa.dataPill(params.trigger.current.assigned_to.email, "string"),
ah_subject: `Incident ${wfa.dataPill(params.trigger.current.number, "string")} assigned to you`,
ah_body: "A new incident has been assigned to you. Please review the details in your queue.",
record: wfa.dataPill(params.trigger.current, "reference"),
table_name: "incident"
}
);
action.core.sendNotification
Sends an in-platform notification using a pre-configured notification template (sysevent_email_action).
When to Use
- Internal ServiceNow user notifications (preferred over
sendEmail) - Multi-channel delivery (email + SMS + push) via a single template
- Centralized template management where Subject/Body live on the notification record
Best Practices
- Resolve the notification by name, not sys_id -- use
lookUpRecordonsysevent_email_action(e.g.,conditions: "name=incident.assigned") rather than hardcoding - Always set
recordso the template can resolve dynamic field values
Important Notes
- Recipients, subject, and body are defined on the template, not the action call -- you can't override them from the flow
- Invalid notification references fail silently -- verify the template exists in System Policy → Email → Notifications
action.core.sendSms
Sends SMS via the email-based SMS gateway. Users must have an SMS device configured.
When to Use
- Critical incident alerts (P1/P0) and on-call notifications
- SLA-breach escalations needing immediate response
- Reserve for urgent / time-sensitive only (per-message cost ~$0.01-0.05)
Best Practices
recipientsrequires template-literal wrapping when using a data pill:recipients: `${wfa.dataPill(user.mobile_phone, "string")}`- E.164 phone format (e.g.,
+14155551234) -- strip spaces, dashes, parentheses - 160-char limit -- lead with incident number, severity, and action required
Important Notes
- SMS can fail silently -- pair with email/notification for critical alerts
- Delivery status is logged in
sys_email
Example
wfa.action(
action.core.sendSms,
{ $id: Now.ID["alert_oncall"] },
{
recipients: `${wfa.dataPill(params.trigger.current.assigned_to.mobile_phone, "string")}`,
message: `URGENT: ${wfa.dataPill(params.trigger.current.number, "string")} requires immediate attention`
}
);
action.core.associateRecordToEmail
Associates a record with a sys_email record by updating the email's Target field.
When to Use
- Link an inbound email to a newly created incident/task/case
- Build an audit trail connecting email correspondence to a record
- Ensure email replies are routed back to the correct record
Best Practices
- Call immediately after creating the related record so downstream actions can query the linked record from the email
- Source
email_recordfrom the trigger -- in inbound-email flows that'sparams.trigger.inbound_email
Important Notes
- Both
target_recordandemail_recordare mandatory - No output -- updates the
targetfield on the email record; calling it again on the same email overwrites the previous target
Example
wfa.action(
action.core.associateRecordToEmail,
{ $id: Now.ID["link_email"] },
{
target_record: wfa.dataPill(incident.record, "reference"),
email_record: wfa.dataPill(params.trigger.inbound_email, "reference")
}
);
action.core.getEmailHeader
Retrieves the value of a specific email header from a sys_email record (first match if duplicates).
When to Use
- Read the
From/Reply-Toheaders for routing decisions - Inspect custom headers like
X-ServiceNow-Generatedto detect platform-generated emails and avoid processing loops
Best Practices
- Guard for missing header -- if the header isn't present,
header_valueis an empty string; check withISNOTEMPTY/ISEMPTYbefore acting - Use standard header names --
From,Reply-To,List-Id,X-ServiceNow-Generated. Names are case-insensitive per RFC 2822 but use the canonical form.
Important Notes
- Returns only the first matching header value
- Output
header_valueis always a string
Example
// Skip processing emails that ServiceNow itself sent
const generated = wfa.action(
action.core.getEmailHeader,
{ $id: Now.ID["check_origin"] },
{
target_header: "X-ServiceNow-Generated",
email_record: wfa.dataPill(params.trigger.inbound_email, "reference")
}
);
wfa.flowLogic.if(
{ $id: Now.ID["external"], condition: `${wfa.dataPill(generated.header_value, "string")}ISEMPTY` },
() => { /* process external email */ }
);
action.core.getLatestResponseTextFromEmail
Extracts the most recent reply text from an email thread, stripping quoted prior messages.
When to Use
- Pull only the user's latest reply for adding as work notes / comments on a record
- Feed clean reply text into keyword detection or sentiment analysis
Best Practices
- Validate/trim the output before writing to a record -- signature blocks and trailing whitespace may remain
- Source
email_recordfrom the trigger (params.trigger.inbound_emailin inbound-email flows)
Important Notes
- Returns only the newest reply -- prior thread history is stripped
- Output
latest_response_textis a plain string
Example
const reply = wfa.action(
action.core.getLatestResponseTextFromEmail,
{ $id: Now.ID["extract_reply"] },
{ email_record: wfa.dataPill(params.trigger.inbound_email, "reference") }
);
wfa.action(
action.core.updateRecord,
{ $id: Now.ID["add_work_note"] },
{
table_name: "incident",
record: wfa.dataPill(params.trigger.current, "reference"),
values: TemplateValue({
work_notes: wfa.dataPill(reply.latest_response_text, "string")
})
}
);
Control Actions
Actions for flow execution control: writing log messages, firing events, and pausing flow execution until a condition is met, an email reply arrives, or a message is received.
For API signatures, parameter tables, and output fields, see the Action API → Control Actions.
action.core.log
Writes custom messages to the flow execution log.
When to Use
- Debugging complex flow logic
- Recording decision points in conditional branches
- Auditing critical operations
Best Practices
- Use sparingly -- avoid adding logs by default (performance impact, log clutter)
- Include context -- record numbers, status values;
"Updated record"with no identifier is useless - Never log PII / passwords / API keys / tokens
- Levels:
'info'(normal),'warn'(non-blocking concern),'error'(failure)
Important Notes
- 255-char message limit; longer values are truncated
log_messagesupports data pills inside template literals
Example
wfa.action(
action.core.log,
{ $id: Now.ID["log_details"] },
{
log_level: "info",
log_message: `Incident ${wfa.dataPill(params.trigger.current.number, "string")} priority=${wfa.dataPill(params.trigger.current.priority, "string")}`
}
);
action.core.fireEvent
Fires a registered ServiceNow system event, triggering any business rules / script actions / notifications subscribed to it.
When to Use
- Trigger downstream legacy automation already built around a system event
- Decouple flow logic from downstream processing by publishing an event others subscribe to
Best Practices
- Pass
event_nameas a plain string -- e.g.,'incident.assigned'. The platform resolves it by name againstsysevent_register; no sys_id lookup needed. - Confirm the event is registered -- firing an unregistered event silently does nothing
- Template-literal wrapping required for
record,parm1,parm2when using data pills
Important Notes
- Fire-and-forget -- no outputs, event handlers run asynchronously outside the flow context
recordis mandatory even if subscribers don't use it
Example
wfa.action(
action.core.fireEvent,
{ $id: Now.ID["fire_event"] },
{
event_name: "third_party.incident.created",
table: "incident",
record: `${wfa.dataPill(newIncident.record, "reference")}`,
parm1: `${wfa.dataPill(params.trigger.from_address, "string")}`,
parm2: `${wfa.dataPill(params.trigger.subject, "string")}`
}
);
action.core.waitForCondition
Pauses flow execution until a specified record matches a condition. Blocking.
When to Use
- Hold a flow until a record reaches a desired state (approval approved, task closed)
- Gate multi-step workflows on an external system updating a ServiceNow record
Best Practices
- Always enable a timeout in production --
timeout_flag: truewith a realistictimeout_duration; handlestate='1'(timeout) with an escalation branch - Use
timeout_schedule(cmn_scheduleref) for business-hours waits -- pauses the clock outside hours so weekend waits don't expire prematurely - Template-literal wrap
recordwhen using data pills
Important Notes
- Output
state:'0'= condition met,'1'= timeout - Conditions use encoded query (e.g.,
state=6^active=false), not JavaScript - Referenced record must already exist when the action runs
Example
const wait = wfa.action(
action.core.waitForCondition,
{ $id: Now.ID["wait_resolved"] },
{
table_name: "task",
record: `${wfa.dataPill(taskRecord.Record, "reference")}`,
conditions: "state=6",
timeout_flag: true,
timeout_duration: Duration({ days: 7 })
}
);
// Branch on timeout
wfa.flowLogic.if(
{ $id: Now.ID["timeout"], condition: `${wfa.dataPill(wait.state, "string")}=1` },
() => { /* escalate */ }
);