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',
},
)
| Parameter | Required | Purpose |
|---|---|---|
activityDefinition | Yes | ActivityDefinitions.Core.* — which activity to instantiate |
config | Yes | Identity, ordering, execution and restart rules |
inputs | No | Data passed to the underlying flow/action |
experienceProperties | No | UI 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
| Definition | Purpose | Key inputs |
|---|---|---|
Instruction | Display a message to the user | message, wait ('yes' / 'no') |
TwoStepInstruction | Two-phase instruction (initial + done) | initial_message, completed_message, skipped_message |
RecordForm | Present a record form for editing | assigned_to, assignment_group |
NewRecordForm | Create a new record via form | table, form_view, template_fields |
AutocompletingRecordForm | Record form that auto-completes on condition | table, record, completion_condition |
KnowledgeArticle | Present a knowledge article | title, knowledge_article, wait |
ChecklistTask | Present a checklist task to the user | assigned_to, assignment_group |
Automation activities
| Definition | Purpose | Key inputs |
|---|---|---|
UpdateRecord | Update field values on a record | table_name, record, values |
CreateNewRecord | Create a new record | table_name, values |
NewRecordFormWithList | Create multiple records in one step | table_name, values |
SendEmail | Send email (automated) | to, cc, bcc, subject, body |
EmailForm | Send email with user input | to, cc, subject, body, wait |
WaitForCondition | Pause until a condition is met | table, record, completion_condition |
Placeholder | Empty placeholder (no behavior) | — |
Approval activities
| Definition | Purpose | Key inputs |
|---|---|---|
RequestManagerApproval | Request approval from the requester's manager | requester, record |
RequestAdHocApproval | Request approval from one or more named approvers | approvers, record |
AskForMultiLevelApproval | Request approval through multiple sequential levels | approval_levels, record |
Branching activities
| Definition | Purpose | Key inputs |
|---|---|---|
Decision | Conditional branching with match_first or match_all | type, branches |
List activities
| Definition | Purpose | Key inputs |
|---|---|---|
RecordList | Display a list of records to the user | assignment_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
inputDisplayPreferencesfilter which of those are settable. - The activity type (e.g.,
Core.List,Core.Decision,Core.Form) determines which experience properties exist; the definition'sexperienceDisplayPreferencesfilter 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 byUpdateRecord,CreateNewRecord,NewRecordFormWithListtable— used byNewRecordForm,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 type | Required format | Example |
|---|---|---|
StringColumn | Plain string | 'incident' |
BooleanColumn | String 'yes' or 'no' | 'yes' |
FieldListColumn | FieldList<'table'>(['field1', 'field2']) | FieldList<'incident'>(['state', 'priority']) |
TemplateValueColumn | TemplateValue({ 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:
| Property | Required | Description |
|---|---|---|
id | Yes | Unique branch identifier, used in decision.branches.<id> |
label | Yes | Display name |
condition | No | Encoded 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:
| Property | Type | Typical use |
|---|---|---|
tagline | string | Short label above the title |
title | string | Main heading |
description | string | Body text (supports HTML) |
icon | string | Icon name (e.g., 'info-circle-outline') |
is_automated | boolean | Mark 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:
| Source | Example |
|---|---|
| Playbook inputs | wfa.playbook.dataPill(params.inputs.record.number) |
| Parent record | wfa.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 state | wfa.playbook.dataPill(validate.state) |
| Activity sys_id | wfa.playbook.dataPill(validate.sysId) |
| Current activity label / description | wfa.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
activitiescallback. - Cross-lane: use the lane variable returned from the
lanescallback, 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
orderfield on the decision config (consistent with laneorder).
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
- Keep inputs minimal. Only set inputs that differ from the definition's defaults — omitting an input uses the definition's
defaultInputsvalue. - 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 aFieldListglobal:FieldList<'table_name'>(['field1', 'field2']). The type parameter scopes the allowed field names to that table.TemplateValueColumn— pass aTemplateValueglobal:TemplateValue({ field1: value1, field2: value2 }). Values can be literals or data pills. BothFieldListandTemplateValueare available globally in.now.tsfiles — do not import them.
- Use
wfa.playbook.dataPill()for runtime values. Never hardcode sys_ids or field values that should come from prior steps — pills resolve at runtime. - Set
restartRuleintentionally. Use'RUN_ONLY_ONCE'for steps that should not repeat (data capture, approval); use'RUN_ALWAYS'for steps that should re-run on restart. - Use
experiencePropertiesfor UI customization only. Keep workflow logic in inputs, not in experience properties. - Declare activities in execution order. Top-to-bottom declaration order matches the execution flow and avoids "variable used before declaration" errors.
- Return every activity from the
activitiescallback. Omitting an activity from the return silently drops it from the lane.
Important Notes
- Activities must be declared as
constvariables and returned from theactivitiescallback using explicitkey: valueform (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'swfa.playbook.dataPill(...)inside the sameactivitiescallback. 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
ActivityConfiginplaybook-api. - Decision branch
idvalues are used fordecision.branches.<id>references — choose descriptive, snake_case identifiers. - The
elsebranch in a Decision has noconditionand 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. Seeplaybook-guidefor the explanation.