Skip to main content
Version: 4.8.0

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 branchingSimple (single lane)
Stages that depend on each otherMulti-lane sequential
Independent work that joins before a final stepMulti-lane parallel with convergence
One-of-N routing based on a record fieldDecision match_first
Several simultaneous side-effects based on conditionsDecision match_all
Routing that depends on a prior decision's resultChained decisions
Conditional routing between whole lanesStage-level decision

Best Practices

  1. Build only the pattern requested. Don't add extra lanes, activities, or branches "just in case."
  2. Use descriptive branch IDs. has_assignee, unassigned, critical_priority are better than branch_1, branch_2.
  3. Always include an else branch. It catches unmatched conditions and prevents surprise stalls.
  4. Use match_first for exclusive routing, match_all for parallel side-effects.
  5. Chain decisions for multi-step routing instead of building one decision with many overlapping conditions.
  6. Use stage-level decisions only for lane routing — for in-lane branching, place the decision inside the lane.
  7. 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 return object of the lanes callback — omitting a lane silently drops it.
  • Every activity must appear in the return object of the activities callback — omitting an activity silently drops it from the lane.
  • At least one lane must use wfa.playbook.run.Immediately() — otherwise nothing will run.
  • order controls display position in Playbook Designer; startRule controls execution order. They are independent.
  • Playbooks deploy in a draft state. Activate the playbook in Playbook Designer before it will execute.