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 */ }
);
action.core.waitForEmailReply
Pauses flow execution until an inbound email reply matches a prior outgoing sys_email. Blocking.
When to Use
- Email-based approvals (user replies "Approved" / "Rejected")
- Collect information from external parties via email before continuing
Best Practices
- Pair with a prior
sendEmail-- use thesendEmailoutput'semailas therecordinput here so replies match watermark_email: trueon the precedingsendEmail-- watermarks are how replies are correlated back to the outgoing email- Always enable timeout -- email replies may never arrive
Important Notes
recordmust be asys_emailreference (not a generic record)- Output
state:'0'= reply received,'1'= timeout - Output
email_replyis the inboundsys_email-- use it to read the reply body/sender - Inbound email processing must be configured on the instance
Example
const sent = wfa.action(
action.core.sendEmail,
{ $id: Now.ID["send_approval_email"] },
{
table_name: "incident",
record: wfa.dataPill(params.trigger.current, "reference"),
ah_to: wfa.dataPill(params.trigger.current.assigned_to.email, "string"),
ah_subject: `Approval required: ${wfa.dataPill(params.trigger.current.number, "string")}`,
ah_body: "Please reply to approve or reject.",
watermark_email: true
}
);
const wait = wfa.action(
action.core.waitForEmailReply,
{ $id: Now.ID["wait_reply"] },
{
record: wfa.dataPill(sent.email, "reference"),
enable_timeout: true,
timeout_duration: Duration({ days: 2 })
}
);
action.core.waitForMessage
Pauses a flow until it receives a specific message string sent via the ServiceNow Flow API. Blocking.
When to Use
- Coordinate with an external system/script that will signal readiness via the Flow API
- Callback-style integrations where an external acknowledgement resumes the flow
Best Practices
- Use unique, descriptive message strings -- include context like a record sys_id (
"provisioning-complete-{sysId}") so the right flow instance resumes - Coordinate the message contract out-of-band -- the external caller (
sn_fd.FlowAPI.resumeFlow(...)) must know the exact string
Important Notes
- Parameter is
timeout(nottimeout_durationlike the other two wait actions -- see the parameter-naming table below) - Message string is case-sensitive -- must match exactly
- Output
payloadis always a string -- if structured data is needed, serialize to JSON - An empty
payloadmeans timeout fired; check withISNOTEMPTY
Example
const msg = wfa.action(
action.core.waitForMessage,
{ $id: Now.ID["wait_provisioning"] },
{
message: "provisioning-complete",
enable_timeout: true,
timeout: Duration({ hours: 24 })
}
);
wfa.flowLogic.if(
{ $id: Now.ID["got_msg"], condition: `${wfa.dataPill(msg.payload, "string")}ISNOTEMPTY` },
() => { /* process the payload */ }
);
Wait action parameter naming differences
The three wait actions use different parameter names for the timeout pattern:
| Action | Boolean flag | Duration parameter |
|---|---|---|
waitForCondition | timeout_flag | timeout_duration |
waitForEmailReply | enable_timeout | timeout_duration |
waitForMessage | enable_timeout | timeout |
Approval Actions
Actions for creating approval records on any ServiceNow record with configurable rule sets.
For API signatures, parameter tables, rule structures, and due-date builder details, see the Action API → Approval Actions.
action.core.askForApproval
Requests approval on a record and waits for the response. Blocking.
When to Use
- Change request approvals (CAB, manager, multi-level)
- Expense / purchase / access-request / contract approvals with threshold-based routing
- Service catalog fulfillment approvals
Best Practices
- Resolve approver sys_ids at runtime via
lookUpRecord-- never hardcode user/group sys_ids - Set
due_dateviawfa.approvalDueDate()-- approvals do not auto-timeout otherwise; flow can wait forever - For >20 approvers, use groups rather than individual users in the rule
- Pick the right
ruleType:'Any'(any single),'All'(consensus),'Res'(all responded then any decides),'Count'(specific N),'Percent'(percentage) - Sequential vs parallel: sequential approvals = multiple
askForApprovalcalls (manager → director → VP); parallel = one call with multiple rule sets (legal + finance)
Important Notes
- Blocks until approval reaches a terminal state (
approved,rejected,cancelled, or due-date auto-action) - Flow continues after rejection -- handle it with
flowLogic.elseIf/else - Creates records in
sysapproval_approver(state, comments, approval_date)
Example
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` },
() => { /* approved path */ }
);
wfa.flowLogic.elseIf(
{ $id: Now.ID["rejected"], condition: `${wfa.dataPill(approval.approval_state, "choice")}=rejected` },
() => { /* rejected path */ }
);
wfa.approvalRules() helper
Builder for askForApproval.approval_conditions. Models approval logic as ruleSets of rules arrays.
Common patterns:
// Any single approver from a user list
wfa.approvalRules({
conditionType: "OR",
ruleSets: [{ action: "Approves", conditionType: "AND",
rules: [[{ ruleType: "Any", users: ["user_1", "user_2"], groups: [], manual: false }]]
}]
});
// All CAB members must approve
wfa.approvalRules({
conditionType: "OR",
ruleSets: [{ action: "Approves", conditionType: "AND",
rules: [[{ ruleType: "All", users: ["cab_1", "cab_2", "cab_3"], groups: [], manual: false }]]
}]
});
// 2 of N approvers
wfa.approvalRules({
conditionType: "OR",
ruleSets: [{ action: "Approves", conditionType: "AND",
rules: [[{ ruleType: "Count", count: 2, users: ["u1","u2","u3","u4","u5"], groups: [], manual: false }]]
}]
});
// 50% of a group
wfa.approvalRules({
conditionType: "OR",
ruleSets: [{ action: "Approves", conditionType: "AND",
rules: [[{ ruleType: "Percent", percent: 50, users: [], groups: ["<group_sys_id>"], manual: false }]]
}]
});
wfa.approvalDueDate() helper
Builder for askForApproval.due_date. Configures automatic action when the due date passes.
Common patterns:
// Auto-reject after 5 calendar days
wfa.approvalDueDate({
action: "reject", dateType: "actual", date: "{}",
duration: 5, durationType: "days", daysSchedule: "" // empty = calendar days
});
// No action on due date (just record it)
wfa.approvalDueDate({
action: "none", dateType: "actual", date: "{}",
duration: 3, durationType: "days", daysSchedule: ""
});
// Relative to a record field's due_date, using business-hours schedule
wfa.approvalDueDate({
action: "approve", dateType: "relative",
date: wfa.dataPill(params.trigger.current.due_date, "glide_date_time"),
duration: 15, durationType: "days",
daysSchedule: "<business_hours_schedule_sys_id>"
});
Task Actions
Actions for creating task records in any task-extended table, with optional blocking semantics for "wait until done" workflows.
For API signatures, parameter tables, and the common task tables reference, see the Action API → Task Actions.
action.core.createTask
Creates a task on any task-extended table. Can optionally pause the flow until the task is completed.
When to Use
- Fulfillment work needing assignment to a person/group (
sc_taskoff catalog request) - Change implementation steps (
change_taskoffchange_request) - Manual review / sign-off steps where the flow must wait for the assignee
Best Practices
- Use the most specific task table (
change_taskovertasketc.) -- drives form layout, assignment rules, reporting - Set
parentinsidefield_valuesto link the task back to the originating record - Provide a meaningful
short_description-- assignees see only this field in their queues - Set
assigned_toorassignment_groupexplicitly -- table defaults often leave tasks unassigned wait: trueblocks -- use sparingly; prefer event-driven follow-ups for long-running fulfillment
Important Notes
- Uses
field_values(notvalueslikecreateRecord/updateRecord) - Outputs are UPPERCASE:
Record,Table(unlikecreateRecord's lowercaserecord) - Different from
createRecord: use this when you want task semantics (state lifecycle, SLA hooks, assignment routing)
Example
const task = wfa.action(
action.core.createTask,
{ $id: Now.ID["implementation_task"] },
{
task_table: "change_task",
wait: true,
field_values: TemplateValue({
parent: wfa.dataPill(params.trigger.current.sys_id, "reference"),
short_description: "Implement approved change",
assignment_group: wfa.dataPill(params.trigger.current.assignment_group, "reference"),
priority: 2
})
}
);
Service Catalog Actions
Actions for programmatic interaction with ServiceNow Service Catalog: submitting requests, populating template variables onto request items, and creating fulfillment tasks.
For API signatures, parameter tables, and outputs, see the Action API → Service Catalog Actions.
action.core.submitCatalogItemRequest
Programmatically orders a catalog item, creating a request item (sc_req_item) on a request (sc_req).
Use only in flows triggered by
trigger.application.serviceCatalog.
When to Use
- Auto-provisioning from upstream automation (e.g., onboarding flow ordering a laptop)
- Bulk ordering driven by a list or import
- Self-service flows that place an order on behalf of a user
Best Practices
- Import the
CatalogItemdefinition and reference with template literal:catalog_item: `${laptopCatalogItem}` - Always check
statusbefore usingrequested_item-- only valid whenstatus='0' catalog_item_inputsuses^-delimited format ("memory=16GB^storage=512GB") -- not commas or JSON- Pair
wait_for_completion: truewithtimeout_flag: true-- otherwise a blocking request can wait indefinitely
Important Notes
- Status codes:
'0'= success,'1'= error (readerror_message),'2'= timeout (request may still be processing) sysparm_quantitydefaults to1if omitted_snc_dont_fail_on_error: truelets the flow continue on submission failure -- still inspectstatusto decide what to do next
Example
import { laptopCatalogItem } from "../catalogs/laptop-catalog";
const request = wfa.action(
action.core.submitCatalogItemRequest,
{ $id: Now.ID["submit_laptop"] },
{
catalog_item: `${laptopCatalogItem}`,
catalog_item_inputs: "memory=16GB^storage=512GB",
sysparm_requested_for: wfa.dataPill(params.trigger.current.sys_id, "reference")
}
);
wfa.flowLogic.if(
{ $id: Now.ID["ok"], condition: `${wfa.dataPill(request.status, "string")}=0` },
() => { /* use request.requested_item */ }
);
action.core.getCatalogVariables
Populates catalog variables on a requested item from a template catalog item. Side-effect only -- no outputs.
Use only in flows triggered by
trigger.application.serviceCatalog.
When to Use
- Surface template variables onto a request item so downstream tasks can read them
- Apply a standard variable set (e.g., compliance template) to existing request items
Best Practices
- Use property references in
catalog_variables-- passcatalogItem.variables.memory(an SDK reference), not a string"memory" - Omit
catalog_variablesto copy all template variables; provide an array to copy a subset
Important Notes
- No outputs -- following actions read variables directly off the request item
template_catalog_itemreferencesst_sys_catalog_items_and_variable_sets(accepts both catalog items and variable sets)- Pre-existing variable values on the target request item may be overwritten
Example
wfa.action(
action.core.getCatalogVariables,
{ $id: Now.ID["fill_from_template"] },
{
requested_item: wfa.dataPill(params.trigger.request_item, "reference"),
template_catalog_item: `${laptopCatalogItem}`,
catalog_variables: [
laptopCatalogItem.variables.memory,
laptopCatalogItem.variables.storage
]
}
);
action.core.createCatalogTask
Creates a catalog task (sc_task) on a request item, with optional template-driven variable population. Blocking by default.
Use only in flows triggered by
trigger.application.serviceCatalog.
When to Use
- Fulfillment hand-off after a catalog request is approved
- Multi-step fulfillment where each step is its own
sc_task - Tasks that need catalog variables surfaced for the fulfiller
Best Practices
- All inputs use the
ah_prefix -- unique to this action; don't mix with unprefixed parameter names - Wrap
ah_fieldsinTemplateValue({...})-- plain strings fail validation - Set
ah_waitexplicitly -- defaults totrue(blocking); setfalsefor fire-and-forget
Important Notes
- Output field is
"Catalog Task"with a space -- must use bracket notation:task["Catalog Task"] ah_table_nameis always'sc_task'(read-only)- Different from
createTask:createCatalogTaskissc_task-only and integrates with catalog variable inheritance back to the request item
Example
const task = wfa.action(
action.core.createCatalogTask,
{ $id: Now.ID["fulfillment_task"] },
{
ah_requested_item: wfa.dataPill(params.trigger.request_item, "reference"),
ah_short_description: "Procure and image standard laptop",
template_catalog_item: `${laptopCatalogItem}`,
catalog_variables: [laptopCatalogItem.variables.memory, laptopCatalogItem.variables.storage],
ah_fields: TemplateValue({
assignment_group: "<fulfillment_group_sys_id>",
priority: "2"
}),
ah_wait: false
}
);
// Bracket notation -- field name has a space
wfa.dataPill(task["Catalog Task"], "reference");
SLA Actions
Actions for SLA-aware flow timing -- pausing execution until a configurable percentage of an SLA's duration has elapsed.
For API signatures, the sla_flow_inputs object fields, and full status semantics, see the Action API → SLA Actions.
action.core.slaPercentageTimer
Pauses the flow until a specified percentage of an SLA's duration has elapsed. Blocking. Resumes when the percentage is reached or the SLA enters a terminal state.
Use only in flows triggered by
trigger.application.slaTask-- this action depends on the SLA trigger's outputs (sla_flow_inputs).
When to Use
- Progressive escalation (notify at 50% / 75% / 90% of SLA time)
- SLA-driven priority raise or breach pre-emption
- Reporting checkpoints at fixed SLA percentages
Best Practices
- Pair with
trigger.application.slaTask-- the trigger suppliessla_flow_inputsso the action targets the right SLA without manual lookup - Always check
status-- only'completed'means the percentage was reached;'paused'/'cancelled'/'skipped'/'repair'indicate the SLA didn't progress normally - Sequential timers for tiered escalation -- 50% timer → action, 75% timer → action, 90% timer → action
Important Notes
percentageis mandatory and must be 0-100; out-of-range values fail at execution- Not a wall-clock wait -- the percentage is computed against the SLA's configured duration (including business-hours schedule), not real elapsed time
task_sla_recordis optional -- when set, the timer locks to that specific SLA record; otherwise the runtime infers it fromsla_flow_inputs
Example
// 75% milestone of an SLA -- pair with trigger.application.slaTask
const sla75 = wfa.action(
action.core.slaPercentageTimer,
{ $id: Now.ID["wait_75"] },
{ percentage: 75 }
);
wfa.flowLogic.if(
{ $id: Now.ID["sla_active"], condition: `${wfa.dataPill(sla75.status, "string")}=completed` },
() => { /* escalate -- raise priority, notify manager, etc. */ }
);
Attachment Actions
Actions for retrieving, copying, moving, deleting, and looking up attachments on records (and emails). All attachment actions enforce server-side validation (ACLs, data policy, business rules); UI policy does not apply.
For API signatures, parameter tables, and output field details, see the Action API → Attachment Actions.
Shared considerations
Action selection at a glance:
| Goal | Action(s) |
|---|---|
| List or count attachments on any record | getAttachmentsOnRecord |
| Copy attachment(s) preserving the original | getAttachmentsOnRecord + forEach + copyAttachment |
| Move attachment(s), removing from source | getAttachmentsOnRecord + forEach + moveAttachment |
| Find a single attachment by file name | lookupAttachment + lookUpRecord (resolve to reference) + downstream |
| Delete attachments (all or by file name) | deleteAttachment |
| Move all email attachments to a record in one call | moveEmailAttachmentsToRecord |
| List or per-attachment process email attachments | lookUpEmailAttachments + forEach + moveAttachment/copyAttachment |
Action characteristics:
| Action | Scope | Destructive? | Outputs |
|---|---|---|---|
getAttachmentsOnRecord | Any record | No | parameter (records), parameter1 (count) |
copyAttachment | Any record | No | none |
moveAttachment | Any record | Yes -- source removed | none |
deleteAttachment | Any record | Yes -- permanent, no undo | none |
lookupAttachment | Any record | No | parameter (sys_id string), parameter1 (JSON list) |
lookUpEmailAttachments | Email only | No | email_attachments (records) |
moveEmailAttachmentsToRecord | Email only | Yes -- email loses attachments | none |
Parameter naming for the attachment-reference input:
| Action | Parameter |
|---|---|
copyAttachment | attachment_record |
moveAttachment | source_attachment_record |
Casing gotcha: lookupAttachment uses lowercase u; lookUpEmailAttachments uses camelCase U. Match the SDK export exactly.
action.core.getAttachmentsOnRecord
Returns the full list and count of attachments on a record. Pair with forEach to process each.
When to Use
- Iterate over each attachment on a record (e.g., copy to a related record)
- Conditionally process only when attachments exist (
parameter1 > 0) - Filter attachments by
file_namewhen scoping to a single file
Best Practices
source_recordrequires template-literal wrapping --source_record: `${wfa.dataPill(..., "reference")}`. Plain data pills will not work.- Guard
forEachwithparameter1 > 0-- avoid empty-loop overhead - Use type
'records'when feedingparameterintoforEach file_namefilters at the source -- cheaper than iterating + filtering inside the loop
Important Notes
- Empty result is safe (
parameterempty,parameter1: 0)
Example
const attachments = wfa.action(
action.core.getAttachmentsOnRecord,
{ $id: Now.ID["get_attachments"] },
{ source_record: `${wfa.dataPill(params.trigger.current, "reference")}` }
);
wfa.flowLogic.if(
{ $id: Now.ID["has"], condition: `${wfa.dataPill(attachments.parameter1, "integer")}>0` },
() => {
wfa.flowLogic.forEach(
wfa.dataPill(attachments.parameter, "records"),
{ $id: Now.ID["each"] },
record => {
wfa.action(
action.core.copyAttachment,
{ $id: Now.ID["copy"] },
{
table: "incident",
target_record: wfa.dataPill(params.trigger.current.parent_incident, "reference"),
attachment_record: wfa.dataPill(record, "reference")
}
);
}
);
}
);
action.core.copyAttachment
Copies one attachment to a target record. The original is preserved.
When to Use
- Replicate attachments between related records (parent ↔ child)
- Mirror email attachments onto a created incident while keeping them on the email
- "Snapshot" records that retain evidence copied from a source
Best Practices
attachment_recordmust be a reference -- not a string sys_id. If you have a string fromlookupAttachment.parameter, resolve vialookUpRecordonsys_attachmentfirst.- Keep
tableandtarget_recordconsistent --tablemust match the table oftarget_record
action.core.moveAttachment
Moves an attachment to a target record.
When to Use
- Promote attachments from staging to final destination
- Reassign attachments when merging two records
- Final hand-off where the source record should no longer retain the file
Best Practices
- Prefer
copyAttachmentwhen in doubt -- this is irreversible from the action's perspective
action.core.deleteAttachment
Permanently deletes attachments from a record. Supports delete-all or delete-by-name.
When to Use
- Cleanup on record closure (remove sensitive evidence)
- Replace prior versions of a file by name before re-upload
- Purge attachments when a record is archived
Best Practices
- Set
delete_all_explicitly -- defaults totrue; setting it makes intent obvious delete_all_: false+attachment_file_namewhen targeting a single file -- otherwise EVERY attachment is deleted
Important Notes
delete_all_ends in an underscore -- exact SDK parameter name- If multiple attachments share
attachment_file_name, all matches are deleted
Example
// Delete all attachments on closure
wfa.action(
action.core.deleteAttachment,
{ $id: Now.ID["wipe_on_close"] },
{
table: "incident",
record: wfa.dataPill(params.trigger.current, "reference"),
delete_all_: true
}
);
// Or delete a single file by name
wfa.action(
action.core.deleteAttachment,
{ $id: Now.ID["delete_temp_pdf"] },
{
table: "incident",
record: wfa.dataPill(params.trigger.current, "reference"),
delete_all_: false,
attachment_file_name: "temp_report.pdf"
}
);
action.core.lookupAttachment
Looks up an attachment by file name and returns its sys_id as a string plus a JSON listing of all attachments.
When to Use
- Find a specific attachment by name without iterating all attachments
- Pre-resolve a sys_id before a
copyAttachment/moveAttachmentcall
Best Practices
parameteris a STRING, not a reference -- you cannot pass it directly tocopyAttachment.attachment_record. Resolve vialookUpRecordonsys_attachmentfirst.- Guard with
ISNOTEMPTYbefore resolving - Always supply
file_name-- without it the action returns the first attachment encountered
Important Notes
- First-match semantics -- when multiple files share a name,
parameterreturns the first sys_id;parameter1is a JSON listing of all matches
Example
const lookup = wfa.action(
action.core.lookupAttachment,
{ $id: Now.ID["find_contract"] },
{
source_record: wfa.dataPill(params.trigger.current, "reference"),
file_name: "contract.pdf"
}
);
wfa.flowLogic.if(
{ $id: Now.ID["found"], condition: `${wfa.dataPill(lookup.parameter, "string")}ISNOTEMPTY` },
() => {
// Resolve the string sys_id to a sys_attachment reference
const resolved = wfa.action(
action.core.lookUpRecord,
{ $id: Now.ID["resolve"] },
{
table: "sys_attachment",
conditions: `sys_id=${wfa.dataPill(lookup.parameter, "string")}`
}
);
wfa.action(
action.core.copyAttachment,
{ $id: Now.ID["copy"] },
{
table: "incident",
target_record: wfa.dataPill(params.trigger.current.parent_incident, "reference"),
attachment_record: wfa.dataPill(resolved.Record, "reference")
}
);
}
);
action.core.lookUpEmailAttachments
Retrieves attachment records associated with a specific email record from sys_email_attachment.
When to Use
- Per-attachment processing of inbound email attachments (filter, transform, route)
- When
moveEmailAttachmentsToRecordis too coarse (you need conditional/filtered handling)
Best Practices
- Use for email records only -- targets
sys_email_attachment. UsegetAttachmentsOnRecordfor non-email records. - Pair with
forEach-- the output is arecordscollection
Important Notes
- No
file_namefilter -- filter inside theforEachif needed
action.core.moveEmailAttachmentsToRecord
Moves all email attachments to a target record in a single call. No forEach required.
When to Use
- Inbound-email handler that creates an incident and pulls all attachments onto it
- One-shot migrations of email payloads to a destination record
Best Practices
- Create the target record first -- run
createRecordto get the target sys_id before the move - Don't use for per-attachment logic -- this moves everything; use
lookUpEmailAttachments+forEach+moveAttachmentfor filtering - Platform enforces limits on email body size, total attachment size, and attachment count per email
Important Notes
- No filtering -- moves every attachment on the email; subset selection is not supported
Example
// Assumes an inbound-email trigger and a prior createRecord that returned `incident`
wfa.action(
action.core.moveEmailAttachmentsToRecord,
{ $id: Now.ID["move_all_attachments"] },
{
target_record: wfa.dataPill(incident.record, "reference"),
email_record: wfa.dataPill(params.trigger.inbound_email, "reference")
}
);
Flow Logic
Flow logic constructs -- conditional branching (if/elseIf/else), loops (forEach), loop control (exitLoop/skipIteration), and flow termination (endFlow) -- are documented separately:
- Flow Logic Guide -- when to use, best practices, common use cases, and end-to-end examples
- Flow Logic API -- signatures, parameter tables, and condition syntax reference
End-to-End Flow Examples & Operational Rules
For complete flow examples composing these actions with triggers and flow logic (Service Catalog with Approval, Record Iteration, Subflow + Custom Action), see the Flow Guide → End-to-End Examples.
For operational rules (folder locations, single-trigger requirement, background execution, global helpers, condition template literals), see the Flow Guide → Rules & Anti-Patterns.