UNPKG

@servicenow/sdk

Version:
766 lines (621 loc) 25.7 kB
--- tags: [FlowStage, flow stage, stage, wfa.stage, sys_hub_flow_stage, flow tracker, stage tracker, flow phases, guide] --- # Flow Stages Guide Comprehensive guide for declaring and activating flow stages in ServiceNow workflows. Stages track progress of a Flow execution through named phases (e.g. Triage → Approval → Investigation). Two steps are needed: **declare** stages in the flow config header using `FlowStage()`, then **activate** them in the flow body using `wfa.stage()`. ## When to Use - When the flow has distinct phases that should be visually tracked (e.g. Intake → Provisioning → Verification) - When stakeholders need visibility into which phase a flow execution is currently in - When you want to set expected durations for each phase - When child subflows activate stages on behalf of a parent flow --- ## How Stages Work at Runtime ### State Lifecycle Each stage transitions through these states during flow execution: **pending → inProgress → complete** (happy path) - **pending** — Stage is declared but not yet activated. - **inProgress** — `wfa.stage()` was called; the stage is now active. - **complete** — A subsequent `wfa.stage()` call activated the next stage, or the flow finished successfully. - **error** — The flow errored while this stage was inProgress. - **skipped** — The flow completed but this stage was never activated (only visible if `alwaysShow: true`). ### One Active Stage at a Time Only **one** stage can be inProgress at any point during execution. Calling `wfa.stage()` for a new stage automatically marks the previous inProgress stage as **complete**. ```typescript fluent wfa.stage(params.stages.triage) // triage → inProgress wfa.action(...) wfa.stage(params.stages.approval) // triage → complete, approval → inProgress wfa.action(...) wfa.stage(params.stages.closure) // approval → complete, closure → inProgress wfa.action(...) // flow ends // closure → complete ``` ### Declaration Order = Display Order Stages appear in the stage tracker **in the order they are declared** in the `stages: {}` object. The first declared stage is displayed first, regardless of which stage is activated first in the body. ```typescript fluent stages: { intake: FlowStage({ ... }), // Displayed first provisioning: FlowStage({ ... }), // Displayed second verification: FlowStage({ ... }), // Displayed third } ``` ### `alwaysShow` Behavior - **`alwaysShow: false`** (default) — If the flow completes without activating this stage, the stage does **not** appear in the tracker. - **`alwaysShow: true`** — The stage always appears in the tracker. If the flow completes without activating it, the stage shows in the **skipped** state (or the custom label for `skipped` if provided). ### Where the Stage Tracker Appears The stage tracker is displayed in: - **Agent Workspace** — execution details panel - **Service Portal** — request item (RITM) view - **Flow Designer** — execution details when inspecting a flow run --- ## Stages on Subflows Subflows support the **same `stages` pattern** as Flows. Declare stages in the Subflow config header and activate them with `wfa.stage()` in the body. ```typescript fluent import { FlowStage, Subflow, wfa, action } from '@servicenow/sdk/automation' import { StringColumn } from '@servicenow/sdk/core' export const approvalSubflow = Subflow( { $id: Now.ID['approval_subflow'], name: 'Approval Subflow', inputs: { incidentSysId: StringColumn({ label: 'Incident Sys ID', mandatory: true }), }, stages: { review: FlowStage({ label: 'Review', value: 'review', alwaysShow: true, }), outcome: FlowStage({ label: 'Outcome', value: 'outcome', duration: Duration({ hours: 2 }), }), }, }, (params) => { wfa.stage(params.stages.review) wfa.action(action.core.askForApproval, { $id: Now.ID['ask_approval'] }, { table: 'incident', record: wfa.dataPill(params.inputs.incidentSysId, 'reference'), approval_field: 'approval', journal_field: 'work_notes', approval_conditions: wfa.approvalRules({ conditionType: 'OR', ruleSets: [{ action: 'Approves', conditionType: 'AND', rules: [[{ ruleType: 'Any', users: [], groups: [], manual: true }]], }], }), }) wfa.stage(params.stages.outcome) wfa.action(action.core.log, { $id: Now.ID['log_outcome'] }, { log_level: 'info', log_message: 'Approval outcome recorded', }) } ) ``` ### Surfacing Subflow Stages in the Parent Flow (`showSubflowStage`) When a parent Flow invokes a Subflow that has its own stages, set `showSubflowStage: true` on the `wfa.subflow()` call to surface the subflow's internal stages in the parent flow's execution details panel. The platform creates a dedicated `type:'subflow'` stage record whose `value` is the subflow instance's UUID, linking the two stage trees. ```typescript fluent import { Flow, FlowStage, wfa, trigger, action } from '@servicenow/sdk/automation' import { approvalSubflow } from './approval-subflow.now' export const parentFlow = Flow( { $id: Now.ID['parent_flow'], name: 'Parent Flow with Subflow Stages', stages: { triage: FlowStage({ label: 'Triage', value: 'triage' }), approval: FlowStage({ label: 'Approval', value: 'approval', alwaysShow: true }), closure: FlowStage({ label: 'Closure', value: 'closure' }), }, }, wfa.trigger(trigger.record.created, { $id: Now.ID['trigger_1'] }, { table: 'incident', condition: 'priority=1', run_flow_in: 'background', run_on_extended: 'false', run_when_user_list: [], run_when_setting: 'both', run_when_user_setting: 'any', }), (params) => { wfa.stage(params.stages.triage) wfa.action(action.core.log, { $id: Now.ID['log_triage'] }, { log_level: 'info', log_message: 'Triage started', }) // Activate approval stage, then delegate to subflow // showSubflowStage: true surfaces the subflow's review/outcome stages // inside this parent flow's stage tracker wfa.stage(params.stages.approval) const result = wfa.subflow( approvalSubflow, { $id: Now.ID['call_approval'], annotation: 'Run approval subflow', showSubflowStage: true, }, { incidentSysId: wfa.dataPill(params.trigger.current.sys_id, 'reference'), } ) wfa.stage(params.stages.closure) wfa.action(action.core.log, { $id: Now.ID['log_closure'] }, { log_level: 'info', log_message: 'Flow complete', }) } ) ``` --- ## Examples ### Example 1: Basic — Two Sequential Stages A simple flow with two stages that run one after the other. ```typescript fluent import { Flow, FlowStage, wfa, trigger, action } from '@servicenow/sdk/automation' export const incidentHandler = Flow( { $id: Now.ID['incident_flow'], name: 'Incident Handler', stages: { triage: FlowStage({ label: 'Triage', value: 'triage', }), resolution: FlowStage({ label: 'Resolution', value: 'resolution', }), }, }, wfa.trigger(trigger.record.created, { $id: Now.ID['trigger_1'] }, { table: 'incident', condition: '', run_on_extended: 'false', run_flow_in: 'background', run_when_user_list: [], run_when_setting: 'both', run_when_user_setting: 'any', }), (params) => { // Activate the triage stage — all actions below belong to triage wfa.stage(params.stages.triage) wfa.action(action.core.log, { $id: Now.ID['log_triage'] }, { log_level: 'info', log_message: 'Triaging incident', }) // Activate the resolution stage — actions below belong to resolution wfa.stage(params.stages.resolution) wfa.action(action.core.log, { $id: Now.ID['log_resolve'] }, { log_level: 'info', log_message: 'Resolving incident', }) } ) ``` ### Example 2: Full Config — Duration, alwaysShow, and Custom States A stage with all optional properties filled in. ```typescript fluent import { Flow, FlowStage, wfa, trigger, action } from '@servicenow/sdk/automation' export const approvalFlow = Flow( { $id: Now.ID['approval_flow'], name: 'Approval Flow', stages: { approval: FlowStage({ label: 'Approval', value: 'approval', duration: Duration({ days: 1 }), alwaysShow: true, states: { pending: 'Not Yet Requested', inProgress: 'Waiting for Approval', complete: 'Approved', error: 'Rejected', skipped: 'Approval Skipped', }, }), }, }, wfa.trigger(trigger.record.created, { $id: Now.ID['trigger_1'] }, { table: 'incident', condition: '', run_on_extended: 'false', run_flow_in: 'background', run_when_user_list: [], run_when_setting: 'both', run_when_user_setting: 'any', }), (params) => { wfa.stage(params.stages.approval) wfa.action(action.core.log, { $id: Now.ID['log_approval'] }, { log_level: 'info', log_message: 'Approval stage active', }) } ) ``` ### Example 3: Minimal Config — Label and Value Only When you don't need duration, custom states, or alwaysShow, a stage can be very concise. ```typescript fluent import { Flow, FlowStage, wfa, trigger, action } from '@servicenow/sdk/automation' export const cleanupFlow = Flow( { $id: Now.ID['cleanup_flow'], name: 'Cleanup Flow', stages: { cleanup: FlowStage({ label: 'Cleanup', value: 'cleanup', }), }, }, wfa.trigger(trigger.record.created, { $id: Now.ID['trigger_1'] }, { table: 'incident', condition: '', run_on_extended: 'false', run_flow_in: 'background', run_when_user_list: [], run_when_setting: 'both', run_when_user_setting: 'any', }), (params) => { wfa.stage(params.stages.cleanup) wfa.action(action.core.log, { $id: Now.ID['log_cleanup'] }, { log_level: 'info', log_message: 'Cleanup stage active', }) } ) ``` ### Example 4: Stages Inside Conditional Logic The same stage can be activated inside different branches. This is useful when the same logical phase applies regardless of which condition is met. ```typescript fluent import { Flow, FlowStage, wfa, trigger, action } from '@servicenow/sdk/automation' export const priorityRouter = Flow( { $id: Now.ID['priority_flow'], name: 'Priority Router', stages: { triage: FlowStage({ label: 'Triage', value: 'triage' }), investigation: FlowStage({ label: 'Investigation', value: 'investigation', duration: Duration({ hours: 2 }), }), }, }, wfa.trigger(trigger.record.created, { $id: Now.ID['trigger_1'] }, { table: 'incident', condition: '', run_on_extended: 'false', run_flow_in: 'background', run_when_user_list: [], run_when_setting: 'both', run_when_user_setting: 'any', }), (params) => { wfa.stage(params.stages.triage) wfa.action(action.core.log, { $id: Now.ID['log_start'] }, { log_level: 'info', log_message: 'Triage started', }) // Same stage activated in both branches wfa.flowLogic.if( { $id: Now.ID['if_p1'], condition: 'priority=1', annotation: '' }, () => { wfa.stage(params.stages.investigation) wfa.action(action.core.log, { $id: Now.ID['log_p1'] }, { log_level: 'warn', log_message: 'P1 investigation', }) } ) wfa.flowLogic.elseIf( { $id: Now.ID['elseif_p2'], condition: 'priority=2', annotation: '' }, () => { wfa.stage(params.stages.investigation) wfa.action(action.core.log, { $id: Now.ID['log_p2'] }, { log_level: 'info', log_message: 'P2 investigation', }) } ) } ) ``` ### Example 5: Declared-but-Never-Activated Stage A stage can be declared in the header without a corresponding `wfa.stage()` call in the body. This is useful when the stage is only activated by a child subflow, or when you want the stage tracker to display a phase that the flow itself doesn't control. ```typescript fluent import { Flow, FlowStage, wfa, trigger, action } from '@servicenow/sdk/automation' export const myFlow = Flow( { $id: Now.ID['my_flow'], name: 'My Flow', stages: { main: FlowStage({ label: 'Main', value: 'main' }), external: FlowStage({ label: 'External Processing', value: 'external' }), }, }, wfa.trigger(trigger.record.created, { $id: Now.ID['trigger_1'] }, { table: 'incident', condition: '', run_on_extended: 'false', run_flow_in: 'background', run_when_user_list: [], run_when_setting: 'both', run_when_user_setting: 'any', }), (params) => { wfa.stage(params.stages.main) wfa.action(action.core.log, { $id: Now.ID['log_1'] }, { log_level: 'info', log_message: 'Doing main work', }) // "external" is declared but never activated via wfa.stage() here. // It might be activated by a subflow or exist for tracker display only. } ) ``` ### Example 6: Multiple Stages with Mixed Activation A realistic flow combining top-level stages with stages inside conditional branches. ```typescript fluent import { Flow, FlowStage, wfa, trigger, action } from '@servicenow/sdk/automation' export const employeeOnboarding = Flow( { $id: Now.ID['onboarding_flow'], name: 'Employee Onboarding', stages: { intake: FlowStage({ label: 'Intake', value: 'intake', duration: Duration({ hours: 4 }), states: { pending: 'Awaiting Intake', inProgress: 'Processing', complete: 'Intake Done', error: 'Intake Failed', }, }), provisioning: FlowStage({ label: 'Provisioning', value: 'provisioning', duration: Duration({ days: 1 }), alwaysShow: true, }), verification: FlowStage({ label: 'Verification', value: 'verification', }), }, }, wfa.trigger(trigger.record.created, { $id: Now.ID['trigger_1'] }, { table: 'hr_case', condition: '', run_on_extended: 'false', run_flow_in: 'background', run_when_user_list: [], run_when_setting: 'both', run_when_user_setting: 'any', }), (params) => { // Stage 1: Intake (top-level) wfa.stage(params.stages.intake) wfa.action(action.core.log, { $id: Now.ID['log_intake'] }, { log_level: 'info', log_message: 'Starting intake', }) // Stage 2: Provisioning (top-level) wfa.stage(params.stages.provisioning) wfa.action(action.core.log, { $id: Now.ID['log_provision'] }, { log_level: 'info', log_message: 'Provisioning resources', }) // Stage 3: Verification (inside conditional) wfa.flowLogic.if( { $id: Now.ID['if_needs_review'], condition: 'needs_review=true', annotation: '' }, () => { wfa.stage(params.stages.verification) wfa.action(action.core.log, { $id: Now.ID['log_verify'] }, { log_level: 'info', log_message: 'Running verification', }) } ) wfa.action(action.core.log, { $id: Now.ID['log_done'] }, { log_level: 'info', log_message: 'Onboarding complete', }) } ) ``` ### Example 7: Stages Inside tryCatch Stages can be activated inside `tryCatch` try and catch bodies. This is useful when success and failure paths should be tracked as different stages. ```typescript fluent import { Flow, FlowStage, wfa, trigger, action } from '@servicenow/sdk/automation' export const tryCatchStages = Flow( { $id: Now.ID['try_catch_flow'], name: 'TryCatch Stages', stages: { resolution: FlowStage({ label: 'Resolution', value: 'resolution' }), errorHandling: FlowStage({ label: 'Error Handling', value: 'errorHandling' }), }, }, wfa.trigger(trigger.record.created, { $id: Now.ID['trigger_1'] }, { table: 'incident', condition: '', run_on_extended: 'false', run_flow_in: 'background', run_when_user_list: [], run_when_setting: 'both', run_when_user_setting: 'any', }), (params) => { wfa.action(action.core.log, { $id: Now.ID['log_start'] }, { log_level: 'info', log_message: 'Starting processing', }) wfa.flowLogic.tryCatch( { $id: Now.ID['tc_1'] }, { try: () => { wfa.stage(params.stages.resolution) wfa.action(action.core.updateRecord, { $id: Now.ID['update_1'] }, { table_name: 'incident', record: wfa.dataPill(params.trigger.current.sys_id, 'reference'), values: TemplateValue({ work_notes: 'Resolved' }), }) }, catch: () => { wfa.stage(params.stages.errorHandling) wfa.action(action.core.log, { $id: Now.ID['log_err'] }, { log_level: 'error', log_message: 'Update failed', }) }, } ) } ) ``` --- ## Rules and Constraints ### Property key must match `value` The stage property key in the `stages: {}` object **must** equal the `value` field inside `FlowStage()`. ```typescript fluent // ✅ Correct — key matches value stages: { triage: FlowStage({ label: 'Triage', value: 'triage' }), } // ❌ Wrong — key "myKey" differs from value "triage" stages: { myKey: FlowStage({ label: 'Triage', value: 'triage' }), } ``` ### `wfa.stage()` must reference a declared stage Every `wfa.stage(params.stages.<key>)` call must reference a key that exists in the `stages: {}` config. Referencing an undeclared key produces a build error. ```typescript fluent // ❌ Build error — "cleanup" is not declared in stages config wfa.stage(params.stages.cleanup) ``` ### Stages cannot be activated inside `forEach` `wfa.stage()` inside a `forEach` body is not supported by the platform and produces a build error. ```typescript fluent // ❌ Build error wfa.flowLogic.forEach({ $id: Now.ID['loop'] }, (item) => { wfa.stage(params.stages.processing) // Not allowed wfa.action(...) }) ``` ### Stages cannot be activated inside `DoInParallel` `wfa.stage()` inside a `DoInParallel` block is not a supported pattern. ```typescript fluent // ❌ Not supported // DoInParallel is imported from '@servicenow/sdk/automation' DoInParallel({ $id: Now.ID['parallel'] }, () => { wfa.stage(params.stages.step1) // Not allowed wfa.action(...) }, ) ``` ### Place `wfa.stage()` before the first action of that stage `wfa.stage()` marks the beginning of a stage. Place it directly before the action(s) that belong to it. ```typescript fluent // ✅ Correct ordering wfa.stage(params.stages.triage) // Stage begins here wfa.action(action.core.log, ...) // This action belongs to "triage" wfa.action(action.core.log, ...) // This also belongs to "triage" wfa.stage(params.stages.approval) // New stage begins wfa.action(action.core.log, ...) // This belongs to "approval" ``` ### Omitting `duration` defaults to zero If `duration` is not specified in `FlowStage()`, it defaults to zero duration. Use `Duration({...})` to set an expected time. ```typescript fluent // Zero duration (default) FlowStage({ label: 'Quick Step', value: 'quick_step' }) // Explicit duration FlowStage({ label: 'Long Step', value: 'long_step', duration: Duration({ hours: 8 }) }) ``` ### `duration` must use the `Duration()` helper If you provide a `duration`, it must use the `Duration()` helper function. Plain objects are not supported and produce a build error. ```typescript fluent // ❌ Build errorplain object not allowed FlowStage({ label: 'Step', value: 'step', duration: { hours: 4 } }) // ✅ Correctuse Duration() helper FlowStage({ label: 'Step', value: 'step', duration: Duration({ hours: 4 }) }) ``` ### Stages are allowed in `if`, `elseIf`, `else`, and `tryCatch` bodies Conditional and error-handling flow logic blocks are valid nesting contexts for `wfa.stage()`, as long as the stage is followed by at least one action or flow logic step. ```typescript fluent // ✅ Validstage inside if/elseIf/else wfa.flowLogic.if({ $id: Now.ID['cond'], condition: '...', annotation: '' }, () => { wfa.stage(params.stages.investigation) wfa.action(...) }) wfa.flowLogic.else({ $id: Now.ID['else_cond'] }, () => { wfa.stage(params.stages.investigation) wfa.action(...) }) // ✅ Validstage inside tryCatch try/catch bodies wfa.flowLogic.tryCatch({ $id: Now.ID['tc'] }, { try: () => { wfa.stage(params.stages.resolution) wfa.action(...) }, catch: () => { wfa.stage(params.stages.errorHandling) wfa.action(...) }, }) ``` ### Stages cannot be the last statement in a branching logic block `wfa.stage()` must be followed by at least one action or flow logic step inside `if`, `elseIf`, `else`, or `tryCatch` bodies. A trailing stage with nothing after it has no effect and produces a build error. ```typescript fluent // ❌ Build errorstage is the last statement in the if body wfa.flowLogic.if({ $id: Now.ID['cond'], condition: '...' }, () => { wfa.stage(params.stages.triage) }) // ❌ Build errorstage is the last statement in the catch body wfa.flowLogic.tryCatch({ $id: Now.ID['tc'] }, { try: () => { wfa.action(...) }, catch: () => { wfa.stage(params.stages.errorHandling) // Not allowed }, }) // ✅ Correctstage is followed by an action wfa.flowLogic.if({ $id: Now.ID['cond'], condition: '...' }, () => { wfa.stage(params.stages.triage) wfa.action(...) }) ``` ### Stages cannot be the last statement in the flow body `wfa.stage()` must be followed by at least one action or flow logic step in the top-level flow body. A stage at the very end of the flow has no effect and produces a build error. ```typescript fluent // ❌ Build errorstage is the last statement in the flow body (params) => { wfa.action(...) wfa.stage(params.stages.cleanup) // Not allowed } // ✅ Correctstage is followed by an action (params) => { wfa.stage(params.stages.cleanup) wfa.action(...) } ``` --- ## Best Practices 1. **Keep stage names descriptive** — Use labels that clearly communicate the phase to stakeholders viewing the stage tracker. 2. **Match key and value** — Always ensure the property key in `stages: {}` matches the `value` inside `FlowStage()`. 3. **Use `alwaysShow` for visibility** — Set `alwaysShow: true` when stakeholders should see the stage in the tracker even if the flow hasn't reached it yet. 4. **Set realistic durations** — Use `Duration({...})` to communicate expected time for each phase. This helps with SLA tracking and stakeholder expectations. 5. **Use custom state labels** — Override default state labels via `states` when the generic labels (pending, in progress, etc.) don't fit the business context. 6. **Declare before activate** — Every `wfa.stage()` call in the body must reference a stage declared in the config header. 7. **Always follow a stage with an action** — Never place `wfa.stage()` as the last statement in a block or the flow body. A stage must always be followed by at least one action or flow logic step. `Duration()` and `TemplateValue()` are global helpersno import needed. See the `data-helpers-guide` topic for full documentation. ## Next Steps For complete API signatures, all `FlowStage()` config properties, `wfa.stage()` usage, and end-to-end examples, see the [Flow Stages API](../api/flow/flow-stages-api.md).