UNPKG

@servicenow/sdk

Version:
1,267 lines (934 loc) 56.7 kB
--- tags: [wfa, workflow-automation, flow-action, OOB-action, built-in-action, action.core, approval, notification, task, attachment, sla, catalog-action, table-actions, communication-actions, control-actions] --- # 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](../api/flow/action-api.md). ### 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 for `createRecord`/`updateRecord`/`createTask`/`updateMultipleRecords`/`createOrUpdateRecord`/`createCatalogTask`. `TemplateValue` is global -- don't import it. - **Capture outputs as `const`** to chain into downstream actions: `const result = wfa.action(...)` then `wfa.dataPill(result.field, "type")`. Watch the output field casing (some actions use lowercase `record`/`table_name`, others use uppercase `Record`/`Records`/`Count`/`Table` -- see the [Action API](../api/flow/action-api.md) 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 in `forEach`. - **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](../api/flow/action-api.md#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 ```typescript fluent 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 `lookUpRecord` results as the source of truth ##### Example ```typescript fluent 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: false` via `updateRecord`) over delete when audit trail matters - **Inside `forEach`**, wrap the `record` parameter in a template literal: `` record: `${wfa.dataPill(record, "reference")}` `` - **Wrap in `flowLogic.if`** to prevent accidental deletion when conditions aren't fully validated ##### Important Notes - **Permanent and irreversible** -- no rollback. Related records may become orphaned. ##### Example ```typescript fluent // 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`** -- verify `status='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_action` to `'use_first_record'` or `'error'` ##### Important Notes - Returns `error_message` on failure (e.g., ACL denial or no match) ##### Example ```typescript fluent 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 for `forEach`-driven workflows - **Check `Count` before `forEach`** -- guards against empty-array iteration ##### Important Notes - `max_results` default is **1000**; system max is typically 10,000 (configurable) - Empty result is safe (`Count: 0`, `Records: []`) ##### Example ```typescript fluent 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 `lookUpRecords` first** 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 - `status` is `'0'` (success) or `'1'` (error); `count` is records updated; `message` carries error details ##### Example ```typescript fluent 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., `email` for `sys_user`, `serial_number` for `cmdb_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 ```typescript fluent 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](../api/flow/action-api.md#communication-actions). #### Choosing the right communication action - **`sendEmail`** -- External recipients, rich HTML formatting, off-platform delivery - **`sendNotification`** -- 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_body` does NOT support data pills** -- use static strings only. Data pills work in `ah_subject` and `ah_to`. - **Always set `record` and `table_name`** for traceability in the email record's history - **`watermark_email: false`** for 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_email` and on the linked record's history - Sending many individual emails in a `forEach` can trip spam filters -- aggregate into a single summary when possible ##### Example ```typescript fluent 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 `lookUpRecord` on `sysevent_email_action` (e.g., `conditions: "name=incident.assigned"`) rather than hardcoding - **Always set `record`** so 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 - **`recipients` requires 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 ```typescript fluent 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_record` from the trigger** -- in inbound-email flows that's `params.trigger.inbound_email` ##### Important Notes - Both `target_record` and `email_record` are mandatory - No output -- updates the `target` field on the email record; calling it again on the same email overwrites the previous target ##### Example ```typescript fluent 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-To` headers for routing decisions - Inspect custom headers like `X-ServiceNow-Generated` to detect platform-generated emails and avoid processing loops ##### Best Practices - **Guard for missing header** -- if the header isn't present, `header_value` is an empty string; check with `ISNOTEMPTY` / `ISEMPTY` before 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_value` is always a string ##### Example ```typescript fluent // 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_record` from the trigger** (`params.trigger.inbound_email` in inbound-email flows) ##### Important Notes - Returns only the **newest reply** -- prior thread history is stripped - Output `latest_response_text` is a plain string ##### Example ```typescript fluent 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](../api/flow/action-api.md#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_message` supports data pills inside template literals ##### Example ```typescript fluent 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_name` as a plain string** -- e.g., `'incident.assigned'`. The platform resolves it by name against `sysevent_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`, `parm2` when using data pills ##### Important Notes - Fire-and-forget -- no outputs, event handlers run asynchronously outside the flow context - `record` is mandatory even if subscribers don't use it ##### Example ```typescript fluent 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: true` with a realistic `timeout_duration`; handle `state='1'` (timeout) with an escalation branch - **Use `timeout_schedule`** (`cmn_schedule` ref) for business-hours waits -- pauses the clock outside hours so weekend waits don't expire prematurely - **Template-literal wrap `record`** when 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 ```typescript fluent 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 the `sendEmail` output's `email` as the `record` input here so replies match - **`watermark_email: true`** on the preceding `sendEmail` -- watermarks are how replies are correlated back to the outgoing email - **Always enable timeout** -- email replies may never arrive ##### Important Notes - `record` must be a `sys_email` reference (not a generic record) - Output `state`: `'0'` = reply received, `'1'` = timeout - Output `email_reply` is the inbound `sys_email` -- use it to read the reply body/sender - Inbound email processing must be configured on the instance ##### Example ```typescript fluent 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`** (not `timeout_duration` like the other two wait actions -- see the [parameter-naming table below](#wait-action-parameter-naming-differences)) - Message string is **case-sensitive** -- must match exactly - Output `payload` is always a string -- if structured data is needed, serialize to JSON - An empty `payload` means timeout fired; check with `ISNOTEMPTY` ##### Example ```typescript fluent 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](../api/flow/action-api.md#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_date` via `wfa.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 `askForApproval` calls (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 ```typescript fluent 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:** ```typescript fluent // 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:** ```typescript fluent // 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](../api/flow/action-api.md#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_task` off catalog request) - Change implementation steps (`change_task` off `change_request`) - Manual review / sign-off steps where the flow must wait for the assignee ##### Best Practices - **Use the most specific task table** (`change_task` over `task` etc.) -- drives form layout, assignment rules, reporting - **Set `parent`** inside `field_values` to link the task back to the originating record - **Provide a meaningful `short_description`** -- assignees see only this field in their queues - **Set `assigned_to` or `assignment_group` explicitly** -- table defaults often leave tasks unassigned - **`wait: true` blocks** -- use sparingly; prefer event-driven follow-ups for long-running fulfillment ##### Important Notes - Uses **`field_values`** (not `values` like `createRecord`/`updateRecord`) - Outputs are **UPPERCASE**: `Record`, `Table` (unlike `createRecord`'s lowercase `record`) - Different from `createRecord`: use this when you want task semantics (state lifecycle, SLA hooks, assignment routing) ##### Example ```typescript fluent 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](../api/flow/action-api.md#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 `CatalogItem` definition** and reference with template literal: `` catalog_item: `${laptopCatalogItem}` `` - **Always check `status` before using `requested_item`** -- only valid when `status='0'` - **`catalog_item_inputs` uses `^`-delimited format** (`"memory=16GB^storage=512GB"`) -- not commas or JSON - **Pair `wait_for_completion: true` with `timeout_flag: true`** -- otherwise a blocking request can wait indefinitely ##### Important Notes - Status codes: `'0'` = success, `'1'` = error (read `error_message`), `'2'` = timeout (request may still be processing) - `sysparm_quantity` defaults to `1` if omitted - `_snc_dont_fail_on_error: true` lets the flow continue on submission failure -- still inspect `status` to decide what to do next ##### Example ```typescript fluent 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`** -- pass `catalogItem.variables.memory` (an SDK reference), not a string `"memory"` - **Omit `catalog_variables` to 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_item` references `st_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 ```typescript fluent 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_fields` in `TemplateValue({...})`** -- plain strings fail validation - **Set `ah_wait` explicitly** -- defaults to `true` (blocking); set `false` for fire-and-forget ##### Important Notes - Output field is **`"Catalog Task"` with a space** -- must use bracket notation: `task["Catalog Task"]` - `ah_table_name` is always `'sc_task'` (read-only) - Different from `createTask`: `createCatalogTask` is `sc_task`-only and integrates with catalog variable inheritance back to the request item ##### Example ```typescript fluent 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](../api/flow/action-api.md#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 supplies `sla_flow_inputs` so 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 - `percentage` is **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_record` is optional -- when set, the timer locks to that specific SLA record; otherwise the runtime infers it from `sla_flow_inputs` ##### Example ```typescript fluent // 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](../api/flow/action-api.md#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_name` when scoping to a single file ##### Best Practices - **`source_record` requires template-literal wrapping** -- `` source_record: `${wfa.dataPill(..., "reference")}` ``. Plain data pills will not work. - **Guard `forEach` with `parameter1 > 0`** -- avoid empty-loop overhead - **Use type `'records'`** when feeding `parameter` into `forEach` - **`file_name`** filters at the source -- cheaper than iterating + filtering inside the loop ##### Important Notes - Empty result is safe (`parameter` empty, `parameter1: 0`) ##### Example ```typescript fluent 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