UNPKG

@servicenow/sdk

Version:
481 lines (389 loc) 18.9 kB
--- tags: [wfa, workflow-automation, custom action, Action, actionStep, wfa.actionStep, assignActionOutputs, reusable action, OOB step] --- # Workflow Automation Custom Action Guide Guide for creating and invoking reusable custom actions using the Fluent SDK. Custom actions encapsulate a sequence of OOB steps with typed inputs and outputs, and are invoked from any Flow or Subflow via `wfa.action()`. For API signatures, full config-property tables, every `actionStep.*` parameter list, the `assignActionOutputs` helper, the sys_id fallback, and the system `DefaultActionOutputs`, see the [Custom Action API](../api/flow/custom-action-api.md). --- ## When to Use - The same multi-step sequence is needed across multiple flows -- consolidate it as a callable action - Standardize common operations (incident escalation, user provisioning, CMDB upserts, etc.) - Build a domain library of reusable actions for an application - Reduce duplication when the same OOB step sequence appears in several flows - Cross-application reusable logic (invoked via sys_id fallback) **Important:** Custom action bodies are strictly sequential OOB steps. **No flow logic, no nested custom actions, no triggers.** If you need branching or loops, model the logic in the calling **flow** (or use a **subflow**, which does support flow logic). ## Custom Action vs Subflow vs Built-in Action | | Custom Action (`Action()`) | Subflow (`Subflow()`) | Built-in (`action.core.*`) | | -------------------------------- | -------------------------------------- | ------------------------------------------- | ---------------------------------------- | | **Defines reusable logic** | Yes | Yes | No (you consume them) | | **Has typed I/O** | Yes | Yes | Yes (predefined) | | **Body content** | `wfa.actionStep()` calls only | Any flow logic + actions + nested subflows | - | | **Supports `wfa.flowLogic.*`** | **No** (sequential only) | **Yes** | - | | **Invoked via** | `wfa.action()` | `wfa.subflow()` | `wfa.action()` | | **File location** | `fluent/actions/` | `fluent/flows/` | (built into SDK) | | **Body required** | Recommended | Optional (stub allowed) | - | --- ## Core Principles 1. **Single responsibility** -- One clear business function per action. Use verb-phrase names (e.g., "Escalate Incident", "Provision User Account"). 2. **OOB steps only** -- The body contains `wfa.actionStep()` calls and (optionally) one `assignActionOutputs` call. Nothing else. 3. **Sequential execution** -- Steps run in declaration order. There is no `if`/`forEach`/etc. 4. **Capture step outputs** -- Assign `wfa.actionStep()` return values to `const` to chain into downstream steps via `wfa.dataPill()`. 5. **Export the constant** -- Always `export const myAction = Action(...)`. 6. **File location** -- Place actions in `fluent/actions/`. --- ## Rules - **Globals** -- `TemplateValue`, `Time`, `Duration` are available globally. Use `TemplateValue({ ... })` directly; `wfa.TemplateValue` is not a thing. - **Namespaces** -- inside a custom action body use `actionStep.*`; the `action.core.*` namespace is for flows/subflows. **Don't copy parameter names between the two** -- they differ. - **Step output chaining** -- capture step return values: `const result = wfa.actionStep(...)`, then `wfa.dataPill(result.field, "type")` downstream. - **Use `wfa.assignActionOutputs(params.outputs, { ... })`** at the end of the body to expose declared outputs. - **Parameter prefixes are inconsistent across steps** -- see the [naming reference](#parameter-naming-inconsistencies). Wrong prefix is a silent type error. - **Error handling per-step** -- `errorHandlingType: 'dont_stop_the_action'` to continue on step error; default is `'stop_the_action'`. - **`access`** values are `'public'` (default) or `'package_private'`. The legacy `'private'` is rejected at build time. - **Location & export** -- custom actions live in `fluent/actions/`; always `export const`. - **Don't hardcode sys_ids** -- resolve via `lookUpRecord` or pass in as action inputs. - **`__action_status__`** is available on every action invocation (`code`, `message`) -- use it for error branching at the call site even when the action declares no outputs. - **`skip_insert: true`** -- handy for `createRecord` / `createTask` during development to validate field mappings without writing records. - **`watermark_email: true`** -- required on `actionStep.email` if a later `waitForEmailReply` needs to thread back. --- ## Action Constructor ```typescript fluent import { Action, wfa, actionStep } from "@servicenow/sdk/automation"; import { StringColumn, BooleanColumn, ReferenceColumn } from "@servicenow/sdk/core"; export const myAction = Action( config, // metadata + inputs + outputs (+ access, annotation, protectionPolicy) body // params => { ... sequential wfa.actionStep() calls + optional assignActionOutputs ... } ); ``` For the full config property table (including `annotation`, `protectionPolicy`, `access`, `category`), see the [Custom Action API Config Parameters](../api/flow/custom-action-api.md#config-parameters). --- ## Invoking a Custom Action Invoke from a Flow or Subflow with `wfa.action()` -- the same helper used for built-in actions. ```typescript fluent import { escalateIncident } from "../actions/escalate-incident.now"; wfa.action( escalateIncident, { $id: Now.ID["escalate_step"], annotation: "Escalate P1 incident" }, { incident: wfa.dataPill(params.trigger.current, "reference"), reason: "Auto-escalated: Priority 1 incident created" } ); ``` ### sys_id fallback When the custom action's definition isn't importable (cross-application), pass its sys_id as a string. Output access becomes untyped. ```typescript fluent const result = wfa.action( "abc123def4567890abc123def4567890", { $id: Now.ID["external_action_call"] }, { someInput: "..." } ); wfa.dataPill(result.someOutput, "string"); ``` --- ## Parameter naming inconsistencies OOB steps use different parameter-naming conventions per step (`create_record_table_name` vs. `table_name` vs. `table` vs. `lookup_table_name`; `create_record_field_values` vs. `update_record_field_values` vs. `field_values` vs. `fields`). Mistyping a parameter name is a common error. See the canonical reference: [Custom Action API Step parameter naming inconsistencies](../api/flow/custom-action-api.md#step-parameter-naming-inconsistencies). --- ## Assigning Action Outputs Use `wfa.assignActionOutputs()` to return values from the action body to the calling flow. Place it after all steps (and after `wfa.errorEvaluation()` if present). ```typescript fluent export const myAction = Action( { $id: Now.ID['my-action'], name: 'My Action', inputs: { description: StringColumn({ label: 'Description', mandatory: true }), }, outputs: { success: BooleanColumn({ label: 'Success' }), result: StringColumn({ label: 'Result' }), }, }, (params) => { const step = wfa.actionStep( actionStep.createRecord, { $id: Now.ID['create-step'], label: 'Create' }, { create_record_table_name: 'incident', create_record_field_values: TemplateValue({ short_description: wfa.dataPill(params.inputs.description, 'string'), }), } ) wfa.assignActionOutputs(params.outputs, { success: '1', result: `Created: ${wfa.dataPill(step.record.number, 'string')}`, }) } ) ``` **Key rules:** - Output keys must match names declared in the action's `outputs` TypeScript enforces this with autocomplete - Values can be static strings (`'test'`), datapill references (`wfa.dataPill(...)`), or template literals mixing both - Use `'1'` for true and `'0'` for false on boolean outputs --- ## Error Evaluation Use `wfa.errorEvaluation()` to conditionally set the action's status based on step results. This is different from `errorHandlingType` on individual steps error evaluation sets the **overall action status** after all steps complete. ```typescript fluent (params) => { const step = wfa.actionStep( actionStep.createRecord, { $id: Now.ID['step'], label: 'Create' }, { create_record_table_name: 'incident', create_record_field_values: TemplateValue({ active: 'true' }), errorHandlingType: 'stop_the_action', } ) wfa.errorEvaluation([ { label: 'Success Check', condition: `${wfa.dataPill(step.__step_status__.code, 'integer')}!=500`, status: { code: 200, message: 'OK' }, dontTreatAsError: true, }, { label: 'Failure Check', condition: `${wfa.dataPill(step.__step_status__.code, 'integer')}=500`, status: { code: 500, message: 'Step failed' }, }, ]) wfa.assignActionOutputs(params.outputs, { success: '1' }) } ``` **Key rules:** - Conditions are evaluated **in order** the first matching condition wins - `dontTreatAsError: true` means the action is considered successful even though a condition matched - Condition strings use ServiceNow encoded query syntax (`^` for AND, `^OR` for OR) - Status `code` and `message` can be static values or datapill template expressions - Place after all `wfa.actionStep()` calls but before `wfa.assignActionOutputs()` **Error evaluation vs step error handling:** - `errorHandlingType: 'stop_the_action'` on a step controls whether the action halts when that specific step fails - `wfa.errorEvaluation()` evaluates conditions after steps complete and sets the overall action status code/message --- ## Combined Example: Error Evaluation + Output Assignment The following example demonstrates both `wfa.errorEvaluation()` and `wfa.assignActionOutputs()` used together in a single action. The step status is checked via error evaluation, and outputs are assigned regardless of the outcome. ```typescript fluent import { Action, wfa, actionStep } from '@servicenow/sdk/automation' import { ReferenceColumn, StringColumn, BooleanColumn } from '@servicenow/sdk/core' export const createAndValidate = Action( { $id: Now.ID['create-validate'], name: 'Create and Validate', inputs: { incident: ReferenceColumn({ label: 'Incident', referenceTable: 'incident', mandatory: true }), reason: StringColumn({ label: 'Reason' }), }, outputs: { success: BooleanColumn({ label: 'Success' }), details: StringColumn({ label: 'Details' }), }, }, (params) => { const createStep = wfa.actionStep( actionStep.createRecord, { $id: Now.ID['create-step'], label: 'Create Record' }, { create_record_table_name: 'incident', create_record_field_values: TemplateValue({ active: 'true' }), errorHandlingType: 'stop_the_action', } ) wfa.errorEvaluation([ { label: 'Created Successfully', condition: `${wfa.dataPill(createStep.__step_status__.code, 'integer')}!=500`, status: { code: 200, message: 'Record created' }, dontTreatAsError: true, }, { label: 'Creation Failed', condition: `${wfa.dataPill(createStep.__step_status__.code, 'integer')}=500`, status: { code: 500, message: wfa.dataPill(createStep.__step_status__.message, 'string') }, }, ]) wfa.assignActionOutputs(params.outputs, { success: '1', details: wfa.dataPill(params.inputs.reason, 'string'), }) } ) ``` **What this shows:** - `errorHandlingType: 'stop_the_action'` on the step halts execution if the step itself fails - `wfa.errorEvaluation()` inspects `createStep.__step_status__.code` to set the overall action status - `dontTreatAsError: true` marks the 200 condition as a success outcome - `wfa.assignActionOutputs()` always runs last values can reference `params.inputs` data pills - Status `message` in the failure condition is itself a data pill from the step status output --- ## Anti-Patterns ### Do NOT use flow logic inside a custom action Custom action bodies are strictly sequential -- if you need `if`/`forEach`, put that logic in the calling **flow** or use a **subflow**. ```typescript fluent // WRONG params => { wfa.flowLogic.if({ ... }, () => { ... }); // NOT allowed }; // CORRECT -- sequential steps only params => { wfa.actionStep(actionStep.createRecord, { ... }, { ... }); wfa.actionStep(actionStep.log, { ... }, { ... }); }; ``` ### Do NOT call another custom action from inside Custom actions cannot nest. Compose at the flow/subflow level. ```typescript fluent // WRONG -- inside an Action() body params => { wfa.action(otherCustomAction, { ... }, { ... }); }; ``` ### Do NOT mix `action.core.*` and `actionStep.*` parameter names ```typescript fluent // WRONG -- ah_* prefix belongs to action.core.sendEmail, NOT actionStep.email wfa.actionStep(actionStep.email, { ... }, { ah_to: "user@x.com", ah_subject: "..." }); // CORRECT wfa.actionStep(actionStep.email, { ... }, { to: "user@x.com", subject: "..." }); ``` ### Do NOT assign data pills to local variables Data pills are evaluated by the platform at runtime, not by JavaScript. Capturing one in a `const` and reusing the variable doesn't work. ```typescript fluent // WRONG const id = wfa.dataPill(params.inputs.recordId, "string"); wfa.actionStep(actionStep.lookUpRecord, { ... }, { conditions: `sys_id=${id}` }); // CORRECT wfa.actionStep( actionStep.lookUpRecord, { ... }, { conditions: `sys_id=${wfa.dataPill(params.inputs.recordId, "string")}` } ); ``` --- ## Patterns ### Basic Custom Action ```typescript fluent import { Action, wfa, actionStep } from "@servicenow/sdk/automation"; import { StringColumn, BooleanColumn } from "@servicenow/sdk/core"; export const logAndCreate = Action( { $id: Now.ID["log_and_create"], name: "Log and Create Incident", description: "Logs the request and creates an incident with the given description and priority", inputs: { description: StringColumn({ label: "Description", mandatory: true }), priority: StringColumn({ label: "Priority", mandatory: true }) }, outputs: { created: BooleanColumn({ label: "Created" }) } }, params => { wfa.actionStep( actionStep.log, { $id: Now.ID["log_start"], label: "Log start" }, { log_message: `Creating incident: ${wfa.dataPill(params.inputs.description, "string")}`, log_level: "info" } ); wfa.actionStep( actionStep.createRecord, { $id: Now.ID["create_incident"], label: "Create incident" }, { create_record_table_name: "incident", create_record_field_values: TemplateValue({ short_description: wfa.dataPill(params.inputs.description, "string"), priority: wfa.dataPill(params.inputs.priority, "string") }) } ); wfa.assignActionOutputs(params.outputs, { created: true }); } ); ``` ### Chaining Step Outputs ```typescript fluent import { Action, wfa, actionStep } from "@servicenow/sdk/automation"; import { ReferenceColumn, StringColumn } from "@servicenow/sdk/core"; export const createAndLog = Action( { $id: Now.ID["create_and_log"], name: "Create Incident and Log Number", inputs: { assignee: ReferenceColumn({ label: "Assignee", referenceTable: "sys_user", mandatory: true }), description: StringColumn({ label: "Description", mandatory: true }) }, outputs: { incidentNumber: StringColumn({ label: "Incident Number" }) } }, params => { const created = wfa.actionStep( actionStep.createRecord, { $id: Now.ID["create_step"], label: "Create incident" }, { create_record_table_name: "incident", create_record_field_values: TemplateValue({ short_description: wfa.dataPill(params.inputs.description, "string"), assigned_to: wfa.dataPill(params.inputs.assignee, "reference") }) } ); wfa.actionStep( actionStep.log, { $id: Now.ID["log_number"], label: "Log incident number" }, { log_message: wfa.dataPill(created.record.number, "string"), log_level: "info" } ); wfa.assignActionOutputs(params.outputs, { incidentNumber: wfa.dataPill(created.record.number, "string") }); } ); ``` ### Continue-on-Error with `errorHandlingType` ```typescript fluent params => { // First step continues even if it errors wfa.actionStep( actionStep.updateRecord, { $id: Now.ID["try_update"] }, { table_name: "incident", record: wfa.dataPill(params.inputs.incident, "reference"), update_record_field_values: TemplateValue({ priority: "1" }), errorHandlingType: "dont_stop_the_action" } ); // This still runs even if the update above failed wfa.actionStep( actionStep.log, { $id: Now.ID["log_after"] }, { log_message: "Update attempted", log_level: "info" } ); }; ``` ### Script Step with Extended Inputs/Outputs The `actionStep.script` step has `allowExtendedInputs: true` and `allowExtendedOutputs: true`, so it accepts `inputVariables` and `outputVariables`. ```typescript fluent import { StringColumn, IntegerColumn } from "@servicenow/sdk/core"; wfa.actionStep( actionStep.script, { $id: Now.ID["calc_metrics"], label: "Calculate metrics" }, { required_run_time: "instance", script: Now.include("./scripts/calc-metrics.js"), inputVariables: { threshold: { label: "Threshold", value: "100" } }, outputVariables: { score: IntegerColumn({ label: "Score", default: "0" }), verdict: StringColumn({ label: "Verdict", maxLength: 64 }) } } ); ``` The `score` and `verdict` outputs become available on the step's return value for downstream `wfa.dataPill()` references. ---