Skip to main content
Version: Latest (4.8.0)

Playbook Activities Guide

An activity is a single step inside a playbook lane — present a form to a user, send an email, update a record, branch on a condition, and so on. Every step a playbook performs is an activity. This guide covers a representative subset of the built-in activities shipped with the SDK: interactive activities (Instruction, RecordForm, KnowledgeArticle, ChecklistTask, …), automation activities (UpdateRecord, CreateNewRecord, SendEmail, …), approval activities, the Decision branching activity, the RecordList display activity, experience properties, and the stage-level Decision pattern (a Decision placed at the lanes body level rather than inside a lane). The full set of exports lives under src/api/playbook/built-ins/activity-definitions/; for wfa.playbook.activity signatures and the data pill validation matrix see playbook-api.

When to Use

  • You need to present an interactive step to a user — a form, checklist, knowledge article, or instruction
  • You need to run automation as part of a workflow step — update a record, create a record, send email
  • You need to branch execution based on data values using a Decision activity
  • You need to configure how a step appears in the playbook UI (title, description, icon)
  • You need to pass runtime values between activities using data pills

Activity call shape

Every activity is a 4-argument call. The fourth argument controls UI presentation and is optional for non-interactive activities.

import { wfa, ActivityDefinitions } from '@servicenow/sdk/automation'

const welcome = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_welcome'],
label: 'Welcome',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{
message: 'Welcome! Please review the details below.',
wait: 'yes',
},
{
tagline: 'Getting Started',
title: 'Welcome',
description: 'Follow the instructions to complete this task.',
icon: 'info-circle-outline',
},
)
ParameterRequiredPurpose
activityDefinitionYesActivityDefinitions.Core.* — which activity to instantiate
configYesIdentity, ordering, execution and restart rules
inputsNoData passed to the underlying flow/action
experiencePropertiesNoUI presentation (tagline, title, icon, form view, …)

Activities are declared as const variables and returned from the lane's activities callback. An activity must be declared before it can be referenced in another activity's wfa.playbook.run.After().

Core Activity Definitions

All definitions are accessed via ActivityDefinitions.Core.*. Import ActivityDefinitions and wfa from @servicenow/sdk/automation.

Interactive activities

DefinitionPurposeKey inputs
InstructionDisplay a message to the usermessage, wait ('yes' / 'no')
TwoStepInstructionTwo-phase instruction (initial + done)initial_message, completed_message, skipped_message
RecordFormPresent a record form for editingassigned_to, assignment_group
NewRecordFormCreate a new record via formtable, form_view, template_fields
AutocompletingRecordFormRecord form that auto-completes on conditiontable, record, completion_condition
KnowledgeArticlePresent a knowledge articletitle, knowledge_article, wait
ChecklistTaskPresent a checklist task to the userassigned_to, assignment_group

Automation activities

DefinitionPurposeKey inputs
UpdateRecordUpdate field values on a recordtable_name, record, values
CreateNewRecordCreate a new recordtable_name, values
NewRecordFormWithListCreate multiple records in one steptable_name, values
SendEmailSend email (automated)to, cc, bcc, subject, body
EmailFormSend email with user inputto, cc, subject, body, wait
WaitForConditionPause until a condition is mettable, record, completion_condition
PlaceholderEmpty placeholder (no behavior)

Approval activities

DefinitionPurposeKey inputs
RequestManagerApprovalRequest approval from the requester's managerrequester, record
RequestAdHocApprovalRequest approval from one or more named approversapprovers, record
AskForMultiLevelApprovalRequest approval through multiple sequential levelsapproval_levels, record

Branching activities

DefinitionPurposeKey inputs
DecisionConditional branching with match_first or match_alltype, branches

List activities

DefinitionPurposeKey inputs
RecordListDisplay a list of records to the userassignment_group, assigned_to, wait

Each activity definition has one backing flow or action (never both) and one activity type. The two pieces drive different parts of the activity's surface:

  • The flow / action determines which inputs (and outputs) exist; the definition's inputDisplayPreferences filter which of those are settable.
  • The activity type (e.g., Core.List, Core.Decision, Core.Form) determines which experience properties exist; the definition's experienceDisplayPreferences filter which of those are settable.

Two definitions sharing the same activity type can expose different experience properties (different filters), and two definitions sharing the same backing flow/action can expose different inputs. See the definition source under src/api/playbook/built-ins/activity-definitions/ for the exact backing flow/action, activity type, and filters applied to each built-in.

Finding Input and Experience Property Names

Before setting any input or experience property value, check the definition file for the activity under src/api/playbook/built-ins/activity-definitions/. Each file declares inputDisplayPreferences and experienceDisplayPreferences — these are the authoritative lists of what the activity accepts. Do not infer input or experience property names from other activities; definitions share backing flows but expose different fields.

Input names vary by definition

Some inputs share the same concept but use different names depending on the activity:

  • table_name — used by UpdateRecord, CreateNewRecord, NewRecordFormWithList
  • table — used by NewRecordForm, AutocompletingRecordForm, WaitForCondition

Always check the definition rather than assuming the name matches a similar activity.

Column types determine value format

Each input and experience property has a column type declared in the backing flow or action definition. The column type determines exactly how the value must be formatted:

Column typeRequired formatExample
StringColumnPlain string'incident'
BooleanColumnString 'yes' or 'no''yes'
FieldListColumnFieldList<'table'>(['field1', 'field2'])FieldList<'incident'>(['state', 'priority'])
TemplateValueColumnTemplateValue({ field: value })TemplateValue({ first_name: 'John' })

FieldList and TemplateValue are available globally in .now.ts files — do not import them.

For TemplateValueColumn inputs such as values, the field names inside TemplateValue({...}) are database column names from the table specified in that activity's table or table_name input. Fluent does not enforce this relationship automatically — the field names must match the target table.

Examples

Instruction

import { wfa, ActivityDefinitions } from '@servicenow/sdk/automation'

const welcome = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_welcome'],
label: 'Welcome',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{
message: 'Welcome! Please review the details below.',
wait: 'yes',
},
{
tagline: 'Step 1',
title: 'Welcome',
description: 'Follow the instructions to complete this task.',
},
)

Set wait: 'yes' when the playbook should pause for user acknowledgment; 'no' for purely informational steps.

RecordForm

import { wfa, ActivityDefinitions } from '@servicenow/sdk/automation'

declare const welcome: any

const review = wfa.playbook.activity(
ActivityDefinitions.Core.RecordForm,
{
$id: Now.ID['act_review'],
label: 'Review Record',
order: 2,
startRule: wfa.playbook.run.After(welcome),
restartRule: 'RUN_ONLY_ONCE',
},
{
assigned_to: '62826bf03710200044e0bfc8bcbe5df1',
},
{
tagline: 'Step 2',
title: 'Record Form',
description: 'Update the record details below.',
form_view: 'default',
show_sla: false,
show_checklist: true,
},
)

UpdateRecord

import { wfa, ActivityDefinitions } from '@servicenow/sdk/automation'

declare const params: any

const acknowledge = wfa.playbook.activity(
ActivityDefinitions.Core.UpdateRecord,
{
$id: Now.ID['act_acknowledge'],
label: 'Acknowledge Incident',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{
table_name: 'incident',
record: wfa.playbook.dataPill(params.parentRecord),
values: TemplateValue({
state: '2',
work_notes: 'Acknowledged via playbook.',
}),
},
{
tagline: 'Step 1',
title: 'Acknowledge',
description: '<p>Incident will move to In Progress state.</p>',
},
)

CreateNewRecord

import { wfa, ActivityDefinitions } from '@servicenow/sdk/automation'

const createChild = wfa.playbook.activity(
ActivityDefinitions.Core.CreateNewRecord,
{
$id: Now.ID['act_create_child'],
label: 'Create Child Incident',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{
table_name: 'incident',
values: TemplateValue({
short_description: 'Child incident created by playbook.',
priority: '2',
}),
},
)

NewRecordFormWithList

import { wfa, ActivityDefinitions } from '@servicenow/sdk/automation'

const createIncident = wfa.playbook.activity(
ActivityDefinitions.Core.NewRecordFormWithList,
{
$id: Now.ID['create_incident'],
label: 'Create Incident',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{},
{
table: 'incident',
},
)

Decision (in-lane)

A Decision activity routes execution to branches. Downstream activities use wfa.playbook.run.After(decision.branches.<id>) to start when a specific branch matches.

import { wfa, ActivityDefinitions } from '@servicenow/sdk/automation'

declare const intake_form: any

const router = wfa.playbook.activity(
ActivityDefinitions.Core.Decision,
{
$id: Now.ID['act_router'],
label: 'Route by Assignment',
order: 2,
startRule: wfa.playbook.run.After(intake_form),
restartRule: 'RUN_ONLY_ONCE',
},
{
type: 'match_first',
branches: [
{
id: 'has_assignee',
label: 'Has Assignee',
condition: `${wfa.playbook.dataPill(intake_form.outputs.record.assigned_to)}ISNOTEMPTY`,
},
{
id: 'unassigned',
label: 'Unassigned',
condition: `${wfa.playbook.dataPill(intake_form.outputs.record.assigned_to)}ISEMPTY`,
},
{ id: 'else', label: 'Else' },
] as const,
},
)

const escalate = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_escalate'],
label: 'Escalate',
order: 3,
startRule: wfa.playbook.run.After(router.branches.unassigned),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'No assignee — routing to on-call.' },
)

Branch structure:

PropertyRequiredDescription
idYesUnique branch identifier, used in decision.branches.<id>
labelYesDisplay name
conditionNoEncoded query with data pills (omit for the else branch)

Branch display order is taken from the array position — the first branch in the branches array has the lowest order, the last has the highest. There is no order field on individual branches.

Match modes:

  • 'match_first' — only the first matching branch executes (if/else if)
  • 'match_all' — all matching branches execute in parallel (multiple if statements)

The else branch has no condition and must be last. 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.

See playbook-patterns-guide for full match_first, match_all, and chained-decision examples.

Experience Properties

The fourth wfa.playbook.activity argument controls UI presentation. Experience properties are per-definition — each activity definition declares its own experienceDisplayPreferences in src/api/playbook/built-ins/activity-definitions/, and the available property set (and naming) varies. Below are properties commonly seen across interactive activities, but you should always check the specific definition to know exactly what's accepted:

PropertyTypeTypical use
taglinestringShort label above the title
titlestringMain heading
descriptionstringBody text (supports HTML)
iconstringIcon name (e.g., 'info-circle-outline')
is_automatedbooleanMark as automated (no user interaction)

For activity-specific properties (form views, record fields, SLA display, list configuration, attachment options, pending-state titles, etc.), open the corresponding definition file and read its experienceDisplayPreferences.

Use wfa.playbook.currentActivity.label and .description inside experience properties to reference the activity's own label/description without triggering a "used before declaration" TypeScript error:

import { wfa, ActivityDefinitions } from '@servicenow/sdk/automation'

const myActivity = wfa.playbook.activity(
ActivityDefinitions.Core.RecordForm,
{ $id: Now.ID['my_form'], label: 'Review Incident', order: 1, startRule: wfa.playbook.run.Immediately(), restartRule: 'RUN_ONLY_ONCE' },
{ /* inputs */ },
{
title: wfa.playbook.dataPill(wfa.playbook.currentActivity.label),
description: wfa.playbook.dataPill(wfa.playbook.currentActivity.description),
},
)

Only label and description are valid on wfa.playbook.currentActivity — any other property access produces a diagnostic error.

Data Pills in Activities

Pass runtime values between activities with wfa.playbook.dataPill(). Pills can dot-walk through reference fields:

import { wfa } from '@servicenow/sdk/automation'

declare const intake_form: any
declare const params: any

wfa.playbook.dataPill(intake_form.outputs.record.assigned_to)
wfa.playbook.dataPill(intake_form.outputs.record.assignment_group)
wfa.playbook.dataPill(params.parentRecord.priority)
wfa.playbook.dataPill(params.inputs.record.short_description)

Pill source summary:

SourceExample
Playbook inputswfa.playbook.dataPill(params.inputs.record.number)
Parent recordwfa.playbook.dataPill(params.parentRecord.short_description)
Activity outputs (same lane)wfa.playbook.dataPill(validate.outputs.record.state)
Activity outputs (cross-lane)wfa.playbook.dataPill(intake.enrich.outputs.support_tier)
Activity statewfa.playbook.dataPill(validate.state)
Activity sys_idwfa.playbook.dataPill(validate.sysId)
Current activity label / descriptionwfa.playbook.dataPill(wfa.playbook.currentActivity.label)

wfa.playbook.dataPill() automatically picks the correct format for the target field (wrapped {{...}} for inputs and experience properties; unwrapped for conditionToRun and Decision branch conditions). See playbook-api for the full pill validation matrix listing which sources are valid in which fields.

Same-lane and cross-lane output references

Activity outputs are accessed through variables, not through params:

  • Same-lane: use local variables declared within the same activities callback.
  • Cross-lane: use the lane variable returned from the lanes callback, then dot-walk to the activity and its outputs.
import { wfa, ActivityDefinitions } from '@servicenow/sdk/automation'

const body = {
lanes: (params: any) => {
const intake = wfa.playbook.lane({
config: { ..., startRule: wfa.playbook.run.Immediately() },
activities: () => {
const review = wfa.playbook.activity(
ActivityDefinitions.Core.RecordForm,
{ $id: Now.ID['act_review'], label: 'Review', order: 1, startRule: wfa.playbook.run.Immediately(), restartRule: 'RUN_ONLY_ONCE' },
{ /* inputs */ },
)
return { review: review }
},
})

const triage = wfa.playbook.lane({
config: { ..., startRule: wfa.playbook.run.After(intake) },
activities: () => {
const categorize = wfa.playbook.activity(
ActivityDefinitions.Core.UpdateRecord,
{ $id: Now.ID['act_categorize'], label: 'Categorize', order: 1, startRule: wfa.playbook.run.Immediately(), restartRule: 'RUN_ONLY_ONCE' },
{
// Cross-lane: intake lane variable → review activity → outputs
record: wfa.playbook.dataPill(intake.review.outputs.record),
},
)
return { categorize: categorize }
},
})

return { intake: intake, triage: triage }
},
}

Cross-lane activity references in startRule are not supported — use a lane-level dependency on config.startRule instead. See playbook-guide for the full explanation.

Stage-Level Decisions

A Decision activity placed at the lanes body level (not inside any lane) routes execution between lanes. Each downstream lane uses wfa.playbook.run.After(decision.branches.<id>) to start when its branch matches.

import { wfa, ActivityDefinitions } from '@servicenow/sdk/automation'

const body = {
lanes: () => {
const triage = wfa.playbook.lane({
config: {
$id: Now.ID['sd_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'],
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['sd_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['sd_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 }
},
})

return { triage: triage, decision: decision, escalation: escalation }
},
}

Key differences from in-lane decisions:

  • Return the decision in the lanes return object alongside the lanes.
  • Use the order field on the decision config (consistent with lane order).

Activity Definition Defaults

Each activity definition can contain a set of default values for inputs and experienceProperties. These default values can be found in the relevant activity definition file in src/api/playbook/built-ins/activity-definitions/, under the defaultInputs and defaultExperienceProperties properties. When a value is NOT provided in the activity call, the default value will be used if available. If a value is provided, it will take priority and the default will not be used. When a playbook is transformed into fluent, all its inputs and experienceProperties will be set in the activity call, so the default values will show up.

Best Practices

  1. Keep inputs minimal. Only set inputs that differ from the definition's defaults — omitting an input uses the definition's defaultInputs value.
  2. Check the definition source to determine input and experience property types. Each built-in definition file under src/api/playbook/built-ins/activity-definitions/ declares the column type for every input and experience property. The column type determines the value format:
    • FieldListColumn — pass a FieldList global: FieldList<'table_name'>(['field1', 'field2']). The type parameter scopes the allowed field names to that table.
    • TemplateValueColumn — pass a TemplateValue global: TemplateValue({ field1: value1, field2: value2 }). Values can be literals or data pills. Both FieldList and TemplateValue are available globally in .now.ts files — do not import them.
  3. Use wfa.playbook.dataPill() for runtime values. Never hardcode sys_ids or field values that should come from prior steps — pills resolve at runtime.
  4. Set restartRule intentionally. Use 'RUN_ONLY_ONCE' for steps that should not repeat (data capture, approval); use 'RUN_ALWAYS' for steps that should re-run on restart.
  5. Use experienceProperties for UI customization only. Keep workflow logic in inputs, not in experience properties.
  6. Declare activities in execution order. Top-to-bottom declaration order matches the execution flow and avoids "variable used before declaration" errors.
  7. Return every activity from the activities callback. Omitting an activity from the return silently drops it from the lane.

Important Notes

  • Activities must be declared as const variables and returned from the activities callback using explicit key: value form (e.g., return { review: review }) — the build transformer reads the property keys statically.
  • An activity must be declared before it can be referenced in another activity's wfa.playbook.run.After() or in a sibling activity's wfa.playbook.dataPill(...) inside the same activities callback. TypeScript catches in-scope forward references as "variable used before declaration." Cross-lane references go through the lane variable (e.g., intake.review.outputs.record) — the lane itself still has to be declared before any other lane references it.
  • Use the ordering fields supported by the config type — see ActivityConfig in playbook-api.
  • Decision branch id values are used for decision.branches.<id> references — choose descriptive, snake_case identifiers.
  • The else branch in a Decision has no condition and must be last.
  • Cross-lane activity references in wfa.playbook.run.After() are not supported — use a lane-level dependency on the lane config instead. See playbook-guide for the explanation.