Skip to main content
Version: Latest (4.7.0)

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.
  • inProgresswfa.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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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().

// ✅ 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.

// ❌ 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.

// ❌ 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.

// ❌ 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.

// ✅ 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.

// 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.

// ❌ Build error — plain object not allowed
FlowStage({ label: 'Step', value: 'step', duration: { hours: 4 } })

// ✅ Correct — use 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.

// ✅ Valid — stage 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(...)
})

// ✅ Valid — stage 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.

// ❌ Build error — stage is the last statement in the if body
wfa.flowLogic.if({ $id: Now.ID['cond'], condition: '...' }, () => {
wfa.stage(params.stages.triage)
})

// ❌ Build error — stage 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
},
})

// ✅ Correct — stage 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.

// ❌ Build error — stage is the last statement in the flow body
(params) => {
wfa.action(...)
wfa.stage(params.stages.cleanup) // Not allowed
}

// ✅ Correct — stage 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 helpers — no 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.