@servicenow/sdk
Version:
ServiceNow SDK
766 lines (621 loc) • 25.7 kB
Markdown
---
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 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.
```typescript fluent
// ✅ 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.
```typescript fluent
// ❌ 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.
```typescript fluent
// ❌ 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](../api/flow/flow-stages-api.md).