Playbook Patterns Guide
Recipes for the most common playbook shapes — simple single-lane workflows, multi-lane sequential and parallel layouts, and decision-driven branching (including chained decisions and stage-level routing between lanes). Each pattern is shown end-to-end so it can be copied as a starting point. For activity and lane reference material see playbook-activities-guide and playbook-lanes-guide.
When to Use
Refer to this guide when designing a new playbook and you need a proven structural starting point. Each pattern can be used as-is or adapted for specific requirements. For conceptual background on how execution ordering and decision branching work, see playbook-guide.
Simple Pattern — Single Lane
Use when one lane is enough. Activities inside the lane can still be sequential, parallel, or branch on a Decision — what makes the pattern "simple" is that there's only one lane, so there's no cross-lane dependency or stage-level routing to coordinate.
Structure: Config → Trigger → Lane → Activities.
Minimal playbook (no triggers, no activities):
import { PlaybookDefinition, wfa } from '@servicenow/sdk/automation'
PlaybookDefinition(
{
$id: Now.ID['simple_playbook'],
label: 'Simple Playbook',
name: 'simple_playbook',
description: '',
},
{},
{ lanes: () => ({}) },
)
Playbook with a trigger and one lane:
import { PlaybookDefinition, PlaybookTriggerTypes, wfa } from '@servicenow/sdk/automation'
PlaybookDefinition(
{
$id: Now.ID['incident_intake'],
label: 'Incident Intake',
name: 'incident_intake',
description: 'Handles intake when a high-priority incident is created',
},
{
triggers: [
wfa.playbook.trigger(
PlaybookTriggerTypes.RecordCreate,
{ $id: Now.ID['trig_incident_create'], label: 'On Incident Created' },
{ table: 'incident', condition: 'priority<=2^active=true' },
),
],
},
{
lanes: () => {
const intake = wfa.playbook.lane({
config: {
$id: Now.ID['lane_intake'],
label: 'Intake Processing',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
return { intake: intake }
},
},
)
Typical use cases: record validation on creation, simple intake/triage, single-team task processing, one-off automation tied to a record event.
Multi-Lane Sequential
Use when work proceeds through clear stages — each stage waits for the previous to finish before it begins.
Structure: Lane 1 (Immediately) → Lane 2 (After Lane 1) → Lane 3 (After Lane 2) → …
import { PlaybookDefinition, wfa } from '@servicenow/sdk/automation'
PlaybookDefinition(
{
$id: Now.ID['sequential_playbook'],
label: 'Sequential Processing',
name: 'sequential_processing',
},
{},
{
lanes: () => {
const intake = wfa.playbook.lane({
config: {
$id: Now.ID['seq_intake'],
label: 'Intake',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
const triage = wfa.playbook.lane({
config: {
$id: Now.ID['seq_triage'],
label: 'Triage',
order: 2,
startRule: wfa.playbook.run.After(intake),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
const investigation = wfa.playbook.lane({
config: {
$id: Now.ID['seq_investigation'],
label: 'Investigation',
order: 3,
startRule: wfa.playbook.run.After(triage),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
const resolution = wfa.playbook.lane({
config: {
$id: Now.ID['seq_resolution'],
label: 'Resolution',
order: 4,
startRule: wfa.playbook.run.After(investigation),
restartRule: 'RUN_ALWAYS',
},
activities: () => ({}),
})
return { intake: intake, triage: triage, investigation: investigation, resolution: resolution }
},
},
)
Typical use cases: pipeline-style processing (triage → investigate → resolve), staged approvals, build/deploy/verify workflows.
Multi-Lane Parallel with Convergence
Use when independent workstreams should run in parallel and then converge before a final stage.
Structure:
Lane A (Immediately) ─┐
├─→ Lane C (After A, B)
Lane B (Immediately) ─┘
import { PlaybookDefinition, wfa } from '@servicenow/sdk/automation'
PlaybookDefinition(
{
$id: Now.ID['parallel_playbook'],
label: 'Parallel Processing',
name: 'parallel_processing',
},
{},
{
lanes: () => {
const team_a = wfa.playbook.lane({
config: {
$id: Now.ID['par_team_a'],
label: 'Team A Work',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
const team_b = wfa.playbook.lane({
config: {
$id: Now.ID['par_team_b'],
label: 'Team B Work',
order: 2,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => ({}),
})
const merge = wfa.playbook.lane({
config: {
$id: Now.ID['par_merge'],
label: 'Merge and Finalize',
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. Add more parallel lanes by adding more arguments.
Typical use cases: multi-team case management, parallel approval and notification workstreams, independent data enrichment that converges for a final decision.
Decision — match_first
Use match_first when execution should follow exactly one branch — the first condition that matches.
import { PlaybookDefinition, ActivityDefinitions, wfa } from '@servicenow/sdk/automation'
PlaybookDefinition(
{ $id: Now.ID['priority_routing'], label: 'Priority Routing', name: 'priority_routing' },
{},
{
lanes: () => {
const routing = wfa.playbook.lane({
config: {
$id: Now.ID['routing_lane'],
label: 'Routing',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => {
const intake_form = wfa.playbook.activity(
ActivityDefinitions.Core.RecordForm,
{
$id: Now.ID['act_intake_form'],
label: 'Intake Form',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{ assigned_to: '' },
)
const router = wfa.playbook.activity(
ActivityDefinitions.Core.Decision,
{
$id: Now.ID['act_router'],
label: 'Route by State',
order: 2,
startRule: wfa.playbook.run.After(intake_form),
restartRule: 'RUN_ONLY_ONCE',
},
{
type: 'match_first',
branches: [
{
id: 'pending',
label: 'Pending',
condition: `${wfa.playbook.dataPill(intake_form.outputs.record.state)}=PENDING`,
},
{
id: 'in_progress',
label: 'In Progress',
condition: `${wfa.playbook.dataPill(intake_form.outputs.record.state)}=IN_PROGRESS`,
},
{ id: 'else', label: 'Else' },
] as const,
},
)
const handle_pending = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_handle_pending'],
label: 'Process Pending',
order: 3,
startRule: wfa.playbook.run.After(router.branches.pending),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'Record is pending — awaiting action.' },
)
const handle_in_progress = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_handle_in_progress'],
label: 'Process In Progress',
order: 4,
startRule: wfa.playbook.run.After(router.branches.in_progress),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'Record is in progress — monitoring.' },
)
const handle_default = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_handle_default'],
label: 'Default Processing',
order: 5,
startRule: wfa.playbook.run.After(router.branches.else),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'Processing with default workflow.' },
)
return { intake_form: intake_form, router: router, handle_pending: handle_pending, handle_in_progress: handle_in_progress, handle_default: handle_default }
},
})
return { routing: routing }
},
},
)
The else branch has no condition and must be the last branch in the array. It runs only when none of the preceding branches matched — it is a fallback, not an "always runs" branch. as const on the branches array is not required (the Decision overload infers literal branch IDs automatically) but is used by convention throughout the codebase — see playbook-anti-patterns-guide.
Decision — match_all
Use match_all when all matching branches should execute, in parallel. Each matching branch's downstream activities start together.
const triage_form = wfa.playbook.activity(
ActivityDefinitions.Core.RecordForm,
{
$id: Now.ID['act_triage_form'],
label: 'Triage Form',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{ assigned_to: '' },
)
const decision = wfa.playbook.activity(
ActivityDefinitions.Core.Decision,
{
$id: Now.ID['act_parallel_check'],
label: 'Check Assignment and Automation',
order: 2,
startRule: wfa.playbook.run.After(triage_form),
restartRule: 'RUN_ONLY_ONCE',
},
{
type: 'match_all',
branches: [
{
id: 'unassigned',
label: 'No Assignee',
condition: `${wfa.playbook.dataPill(triage_form.outputs.record.assigned_to)}ISEMPTY`,
},
{
id: 'was_automated',
label: 'Was Automated',
condition: `${wfa.playbook.dataPill(triage_form.outputs.automated)}=true`,
},
{ id: 'else', label: 'Else' },
] as const,
},
)
const assign_oncall = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_assign_oncall'],
label: 'Assign to On-Call',
order: 3,
startRule: wfa.playbook.run.After(decision.branches.unassigned),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'No assignee found — routing to on-call.' },
)
const log_automation = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_log_automation'],
label: 'Log Automation',
order: 4,
startRule: wfa.playbook.run.After(decision.branches.was_automated),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'Activity was completed via automation.' },
)
If a record is both unassigned and was automated, both assign_oncall and log_automation execute in parallel.
Chained Decisions
Feed one decision into another by starting the second decision after a specific branch of the first.
const check_assignee = wfa.playbook.activity(
ActivityDefinitions.Core.Decision,
{
$id: Now.ID['act_check_assignee'],
label: 'Check Assignee',
order: 2,
startRule: wfa.playbook.run.After(request_form),
restartRule: 'RUN_ONLY_ONCE',
},
{
branches: [
{
id: 'has_assignee',
label: 'Has Assignee',
condition: `${wfa.playbook.dataPill(request_form.outputs.record.assigned_to)}ISNOTEMPTY`,
},
{ id: 'else', label: 'Else' },
] as const,
},
)
// Second decision runs only when there IS an assignee
const check_group = wfa.playbook.activity(
ActivityDefinitions.Core.Decision,
{
$id: Now.ID['act_check_group'],
label: 'Check Assignment Group',
order: 3,
startRule: wfa.playbook.run.After(check_assignee.branches.has_assignee),
restartRule: 'RUN_ONLY_ONCE',
},
{
branches: [
{
id: 'has_group',
label: 'Has Group',
condition: `${wfa.playbook.dataPill(request_form.outputs.record.assignment_group)}ISNOTEMPTY`,
},
{ id: 'else', label: 'Else' },
] as const,
},
)
const route_to_queue = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_route_to_queue'],
label: 'Route to Queue',
order: 4,
startRule: wfa.playbook.run.After(check_assignee.branches.else),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'No assignee — routing to unassigned queue.' },
)
const notify_group = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_notify_group'],
label: 'Notify Group',
order: 5,
startRule: wfa.playbook.run.After(check_group.branches.has_group),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'Notifying assignment group.', wait: 'yes' },
)
Stage-Level Decision — Route Between Lanes
Place a Decision at the lanes body level (not inside any lane) to route execution to different lanes. Each downstream lane uses wfa.playbook.run.After(decision.branches.<id>).
import { PlaybookDefinition, ActivityDefinitions, wfa } from '@servicenow/sdk/automation'
PlaybookDefinition(
{ $id: Now.ID['incident_routing'], label: 'Incident Triage and Routing', name: 'incident_routing' },
{},
{
lanes: () => {
const triage = wfa.playbook.lane({
config: {
$id: Now.ID['sl_triage'],
label: 'Triage',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => {
const review = wfa.playbook.activity(
ActivityDefinitions.Core.RecordForm,
{
$id: Now.ID['act_review_form'],
label: 'Review Incident',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{ assigned_to: '' },
)
return { review: review }
},
})
const decision = wfa.playbook.activity(
ActivityDefinitions.Core.Decision,
{
$id: Now.ID['sl_route_decision'],
label: 'Route by Assignment',
order: 2,
startRule: wfa.playbook.run.After(triage),
restartRule: 'RUN_ONLY_ONCE',
},
{
type: 'match_first',
branches: [
{
id: 'unassigned',
label: 'Unassigned',
condition: `${wfa.playbook.dataPill(triage.review.outputs.record.assigned_to)}ISEMPTY`,
},
{
id: 'has_group',
label: 'Has Group',
condition: `${wfa.playbook.dataPill(triage.review.outputs.record.assignment_group)}ISNOTEMPTY`,
},
{ id: 'else', label: 'Else' },
] as const,
},
)
const escalation = wfa.playbook.lane({
config: {
$id: Now.ID['sl_escalation'],
label: 'Escalation',
order: 3,
startRule: wfa.playbook.run.After(decision.branches.unassigned),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => {
const page = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_page_oncall'],
label: 'Page On-Call',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'Unassigned — paging on-call engineer.' },
)
return { page: page }
},
})
const group_route = wfa.playbook.lane({
config: {
$id: Now.ID['sl_group_route'],
label: 'Group Routing',
order: 4,
startRule: wfa.playbook.run.After(decision.branches.has_group),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => {
const notify = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_notify_group'],
label: 'Notify Group',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'Notifying assignment group.' },
)
return { notify: notify }
},
})
const standard = wfa.playbook.lane({
config: {
$id: Now.ID['sl_standard'],
label: 'Standard Processing',
order: 5,
startRule: wfa.playbook.run.After(decision.branches.else),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => {
const queue = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_queue_task'],
label: 'Queue for Processing',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'Added to standard processing queue.' },
)
return { queue: queue }
},
})
return { triage: triage, decision: decision, escalation: escalation, group_route: group_route, standard: standard }
},
},
)
Stage-level differences:
- Return the decision in the lanes return object alongside the lanes.
Picking the Right Pattern
| If the requirement is… | Use |
|---|---|
| One execution path, no branching | Simple (single lane) |
| Stages that depend on each other | Multi-lane sequential |
| Independent work that joins before a final step | Multi-lane parallel with convergence |
| One-of-N routing based on a record field | Decision match_first |
| Several simultaneous side-effects based on conditions | Decision match_all |
| Routing that depends on a prior decision's result | Chained decisions |
| Conditional routing between whole lanes | Stage-level decision |
Best Practices
- Build only the pattern requested. Don't add extra lanes, activities, or branches "just in case."
- Use descriptive branch IDs.
has_assignee,unassigned,critical_priorityare better thanbranch_1,branch_2. - Always include an
elsebranch. It catches unmatched conditions and prevents surprise stalls. - Use
match_firstfor exclusive routing,match_allfor parallel side-effects. - Chain decisions for multi-step routing instead of building one decision with many overlapping conditions.
- Use stage-level decisions only for lane routing — for in-lane branching, place the decision inside the lane.
- Reference prior activity outputs with
wfa.playbook.dataPill(activity.outputs.record.field)— dot-walk through reference fields rather than splitting outputs across many fields.
Important Notes
- Every lane must appear in the
returnobject of thelanescallback — omitting a lane silently drops it. - Every activity must appear in the
returnobject of theactivitiescallback — omitting an activity silently drops it from the lane. - At least one lane must use
wfa.playbook.run.Immediately()— otherwise nothing will run. ordercontrols display position in Playbook Designer;startRulecontrols execution order. They are independent.- Playbooks deploy in a draft state. Activate the playbook in Playbook Designer before it will execute.