Playbook Lanes Guide
Practical guide for configuring playbook lanes — covers restart rules, conditional execution, start delays, and common lane layouts. For the underlying type signatures see playbook-api; for execution-ordering concepts see playbook-guide.
When to Use
- You need to configure whether a lane re-runs when the playbook is restarted (
restartRule) - You need to skip a lane conditionally at runtime based on a data pill (
conditionToRun) - You need to delay a lane's start after its dependencies complete (
startWithDelay) - You need a layout reference for sequential, parallel, or conditional lane structures
Restart Rules
Every lane declares a restartRule that controls behavior when a playbook is restarted.
| Rule | Behavior |
|---|---|
'RUN_ONLY_ONCE' | Lane runs on the first execution only; skipped on restart |
'RUN_ALWAYS' | Lane runs on every execution, including restarts |
'RUN_ONLY_ON_RESTART' | Lane is skipped on the first execution; runs only when the playbook is restarted |
Use 'RUN_ONLY_ONCE' for setup or intake lanes that should not repeat. Use 'RUN_ALWAYS' for resolution lanes that should re-evaluate state on restart. Use 'RUN_ONLY_ON_RESTART' for cleanup or recovery lanes that only make sense after a prior run.
Conditional Execution
Use conditionToRun to skip a lane at runtime based on a condition. The condition uses ServiceNow encoded query format; interpolate data pills with wfa.playbook.dataPill() and the helper handles formatting for you.
// Assumes the playbook declares a boolean input called `skipEnrichment`
const enrichment = wfa.playbook.lane({
config: {
$id: Now.ID['lane_enrichment'],
label: 'Enrichment',
order: 2,
startRule: wfa.playbook.run.After(intake),
restartRule: 'RUN_ALWAYS',
conditionToRun: `${wfa.playbook.dataPill(params.inputs.skipEnrichment)}=false`,
},
activities: () => ({}),
})
Allowed pill sources in conditionToRun:
params.inputs.*— playbook inputsparams.parentRecord.*— the triggering record's fields- Activity outputs from a lane that runs before this one
- Activity state from a lane that runs before this one
Not allowed in conditions: params.state, activity sysId, and wfa.playbook.currentActivity.* — the pill resolver emits a diagnostic error if you use any of these in a conditionToRun (or any other condition field).
Operators: Use encoded query syntax — = equals, != not equals, STARTSWITH, ENDSWITH, LIKE, ISEMPTY, ISNOTEMPTY, ^ AND, ^OR OR.
For conditional routing within a lane (rather than skipping the lane entirely), prefer a Decision activity. Use conditionToRun only when the whole lane should be skipped.
Start With Delay
startWithDelay delays a lane's start after its startRule is satisfied. Three delay types are supported, discriminated by type.
Explicit duration
Fixed delay specified as any combination of days, hours, minutes, and seconds:
const followup = wfa.playbook.lane({
config: {
$id: Now.ID['lane_followup'],
label: 'Follow-Up',
order: 2,
startRule: wfa.playbook.run.After(intake),
restartRule: 'RUN_ONLY_ONCE',
startWithDelay: {
type: 'explicit',
duration: { days: 4, hours: 3, minutes: 2, seconds: 1 },
},
},
activities: () => ({}),
})
duration accepts a data pill in addition to the literal { days, hours, minutes, seconds } object.
Relative duration
Delay relative to a datetime, before or after. Both duration and relativeDatetime accept data pills as well as literal values.
startWithDelay: {
type: 'relative',
duration: { days: 5, hours: 4 },
relativeDatetime: '2026-03-20 22:13:31',
relativeOperator: 'after',
}
relativeOperator is 'before' or 'after'.
Percentage duration
Delay as a percentage of time relative to a datetime:
startWithDelay: {
type: 'percentage',
percentage: 50,
percentageDatetime: '2026-03-20 22:13:57',
}
percentage is greater than 0 and up to 100; decimals like 10.5 are allowed. Both percentage and percentageDatetime accept data pills as well as literals.
When to use which
- Use
explicitfor fixed offsets ("start 1 hour after the previous lane completes"). - Use
relativewhen the offset is computed against a known datetime, optionally from a data pill. - Use
percentagefor SLA-style timing ("start when 50% of the SLA window has elapsed").
Prefer startWithDelay over inserting a timer activity as the first step of a lane — the platform handles the delay natively and the intent is clearer in the lane configuration.
Common Lane Layouts
Single lane — runs immediately
lanes: () => {
const intake = wfa.playbook.lane({
config: {
$id: Now.ID['lane_intake'],
label: 'Intake',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
return { intake: intake }
}
Sequential lanes — each waits for the previous
lanes: () => {
const triage = wfa.playbook.lane({
config: {
$id: Now.ID['lane_triage'],
label: 'Triage',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
const resolution = wfa.playbook.lane({
config: {
$id: Now.ID['lane_resolution'],
label: 'Resolution',
order: 2,
startRule: wfa.playbook.run.After(triage),
restartRule: 'RUN_ALWAYS',
},
activities: () => ({}),
})
return { triage: triage, resolution: resolution }
}
Convergence — one lane waits for two parallel lanes
lanes: () => {
const team_a = wfa.playbook.lane({
config: {
$id: Now.ID['lane_team_a'],
label: 'Team A',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
const team_b = wfa.playbook.lane({
config: {
$id: Now.ID['lane_team_b'],
label: 'Team B',
order: 2,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
const merge = wfa.playbook.lane({
config: {
$id: Now.ID['lane_merge'],
label: 'Merge Results',
order: 3,
startRule: wfa.playbook.run.After(team_a, team_b),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
return { team_a: team_a, team_b: team_b, merge: merge }
}
wfa.playbook.run.After(team_a, team_b) accepts varargs — the merge lane waits for both dependencies to complete.
Conditional lane with delay
lanes: (params) => {
const intake = wfa.playbook.lane({
config: {
$id: Now.ID['lane_intake'],
label: 'Intake',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
const triage = wfa.playbook.lane({
config: {
$id: Now.ID['lane_triage'],
label: 'Triage',
order: 2,
startRule: wfa.playbook.run.After(intake),
restartRule: 'RUN_ALWAYS',
conditionToRun: `${wfa.playbook.dataPill(params.inputs.skipEnrichment)}=false`,
startWithDelay: {
type: 'explicit',
duration: { hours: 1 },
},
},
activities: () => ({}),
})
return { intake: intake, triage: triage }
}
Best Practices
- Declare lanes in dependency order. A lane must be assigned to a
constbefore another lane can reference it inwfa.playbook.run.After(). TypeScript will catch out-of-order references as "variable used before declaration." - Always return every lane variable. Each
constlane must appear in the object returned by thelanesfunction — otherwise the lane is not registered. - Use
orderfor visual layout only.ordercontrols display position in the designer;startRulecontrols execution order. They are independent. - At least one lane should start with
wfa.playbook.run.Immediately(). Otherwise nothing will run. - Use
conditionToRunonly to skip the whole lane. For conditional routing inside a lane, use a Decision activity. - Prefer
startWithDelayover a leading timer activity. When the intent is "delay this lane's start," express it in the lane config rather than as a step. - Use descriptive lane labels. Labels appear in the playbook UI and in restart prompts.
Important Notes
- A lane with
wfa.playbook.run.After(a, b)waits for all listed dependencies to complete. - The
activitiescallback must return an object. Use() => ({})for an empty lane. - Cross-lane data references use the lane variable:
intake.enrich.outputs.support_tier. Cross-lane activity references in an activity'sstartRuleare not supported — use a lane-level dependency instead. Seeplaybook-guidefor the full explanation.