Skip to main content
Version: 4.11.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, how to read a built-in definition's backing flow/action and activity type files, AI agent configuration on supported form activities, 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
SetPlaybookOutputsAssign values to the playbook's declared outputsplaybook_outputs
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.

Reading a Built-In Activity Definition

Every entry under ActivityDefinitions.Core.* is assembled from up to three files. To fully understand what an activity accepts and how it behaves, trace all three:

FileLocationDeclares
Definitionsrc/api/playbook/built-ins/activity-definitions/<name>.now.tsThe ActivityDefinition({...}) call — identity, which flow/action and activity type back it, and which of their fields are author-facing
Backing flow/actionsrc/api/playbook/built-ins/backing-flows/<name>.now.ts or backing-actions/<name>.now.tsThe Subflow({...})/Action({...}) behind flow/action — the real inputs/outputs schema
Activity typesrc/api/playbook/built-ins/activity-types/<name>.now.tsThe ActivityType({...}) behind activityType — the real experienceProperties schema

Definition-level properties

Every field on an ActivityDefinition({...}) call, using RecordForm's definition as a worked example:

export const RecordForm = ActivityDefinition({
$id: 'c9839e38b701311004c164fdde11a904',
label: 'Record Form',
flow: manualActivity,
activityType: ActivityTypes.Core.Record,
enableAiAgent: 'on',
aiAgentObjective: '...',
aiAgentRunAs: 'playbook_user',
aiAgentExecutionMode: 'on',
aiAgentFieldDisplayPreferences: { enable_ai_agent: 'all', /* ... */ },
inputDisplayPreferences: { assigned_to: 'standard', assignment_group: 'standard' },
experienceDisplayPreferences: { associated_table: 'all', /* ... */ },
defaultExperienceProperties: { description: '{{act.act_instance.description}}', /* ... */ },
})
  • $id — sys_id of the real sys_pd_activity_definition platform record. Every built-in's $id matches an existing platform record; this is how Fluent maps ActivityDefinitions.Core.RecordForm back to a real activity type when transforming XML.
  • label — display name shown in Playbook Designer.
  • flow or action — exactly one is set, never both. Points at a Subflow(...) (from backing-flows/) or Action(...) (from backing-actions/) that declares the activity's real inputs/outputs schema — see Tracing the backing flow/action below.
  • activityType — points at an ActivityType(...) (from activity-types/) that declares the real experienceProperties schema — see Tracing the activity type below.
  • inputDisplayPreferences — a Record<inputName, 'all' | 'standard' | 'advanced'> naming exactly which of the backing flow/action's inputs an author is allowed to set. Anything not listed here is internal-only, even though it exists on the backing flow/action. See Finding Input and Experience Property Names below.
  • experienceDisplayPreferences — the same idea for experienceProperties, filtering the activity type's full schema down to what's author-facing.
  • defaultInputs / defaultExperienceProperties — values applied automatically when an author omits that field. See Activity Definition Defaults below.
  • enableAiAgent, aiAgentObjective, aiAgentObjAdditionalDetail, aiAgentRunAs, * *aiAgentExecutionMode**, aiAgentFieldDisplayPreferences — only present on AI-capable built-ins (RecordForm, NewRecordForm, EmailForm, AutocompletingRecordForm). See AI Agent Configuration below.

Tracing the backing flow/action

Open the file under backing-flows/ or backing-actions/ referenced by flow/action. It's a Subflow({...}) or Action({...}) (from ../helpers/flow) with inputs and outputs objects, each field a Column call carrying a label, a sysId (the platform variable's sys_id), and type-specific options. RecordForm's backing flow ( backing-flows/manual-activity.now.ts):

export const manualActivity = Subflow({
$id: 'cb18ceef1b230010affd0e55cc4bcbf2',
name: 'Manual Activity',
inputs: {
assigned_to: ReferenceColumn({ label: 'Assigned To', sysId: '...', referenceTable: 'sys_user' }),
assignment_group: ReferenceColumn({ label: 'Assignment Group', sysId: '...', referenceTable: 'sys_user_group' }),
wait: ChoiceColumn({ label: 'Wait for user input', sysId: '...', choices: { yes: { label: 'Yes' }, no: { label: 'No' } } }),
},
outputs: {
record: ReferenceColumn({ label: 'Record', referenceTable: 'sys_flow_data' }),
automated: BooleanColumn({ label: 'Automated' }),
},
})

This is the complete set of inputs the underlying flow supports — cross-reference against the definition's inputDisplayPreferences to see which of these (assigned_to, assignment_group) are actually author-facing on RecordForm (wait isn't listed, so it's internal-only for this activity even though the flow itself declares it).

Tracing the activity type

Open the file under activity-types/ referenced by activityType. It's an ActivityType({...}) with an experienceProperties object, same Column-based schema. An excerpt from RecordForm's activity type ( activity-types/record.now.ts):

export const RecordActivityType = ActivityType({
$id: 'e12af577871333003058d1a936cb0ba4',
experienceProperties: {
associated_table: TableNameColumn({ label: 'Associated Table', sysId: '...', columnType: 'string' }),
associated_record: DocumentIdColumn({ label: 'Associated Record', sysId: '...', columnType: 'string', dependent: 'associated_table' }),
record_fields: FieldListColumn({ label: 'Record Fields', sysId: '...', columnType: 'field_list', dependent: 'associated_table' }),
// ...
},
})

The dependent option marks a field that only makes sense once another field is set — e.g. associated_record/ record_fields are dependent: 'associated_table' because you can't pick a record or field list until the table is known. Designer greys these out until their dependency is set; TypeScript typing and Fluent compilation enforces this relationship for known tables, and will error if a record from the wrong table is provided.

Activity types are shared across definitions — several built-ins reference the same ActivityType, each filtering it down to a different author-facing subset via their own experienceDisplayPreferences.

Column type reference

Every inputs/outputs/experienceProperties field is one of these Column types (imported from @servicenow/sdk-core/db in the backing flow/action/activity-type files — you won't import these yourself, but recognizing them tells you the value format an activity's input/experience property expects):

Column typeRequired Fluent value formatExample
StringColumn / HtmlColumnPlain string'incident'
BooleanColumnString 'yes' or 'no' (activity inputs)'yes'
ChoiceColumnOne of the column's declared choices keys, as a string'yes'
ReferenceColumnsys_id string (or a data pill) of a record on referenceTable'62826bf0...'
DocumentIdColumnsys_id string (or a data pill) of a record on whatever table its dependent field resolves towfa.playbook.dataPill(params.parentRecord)
TableNameColumnA table name string'incident'
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.

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.

Each input and experience property has a column type declared in the backing flow, action, or activity type file — see * Column type reference* in Reading a Built-In Activity Definition above for the full list of column types and the value format each expects.

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',
},
)

SetPlaybookOutputs

SetPlaybookOutputs writes values into the playbook's declared outputs so downstream consumers can read them via pd.outputs.<name> pills. Unlike other activities, its input schema is dynamic: the single playbook_outputs input is a TemplateValue whose keys are the output element names declared in the playbook's outputs (see the playbook config outputs in playbook-api). Values may be literals or data pills.

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

PlaybookDefinition(
{
$id: Now.ID['pb_set_outputs'],
label: 'Set Outputs Example',
name: 'set_outputs_example',
parentTable: 'incident',
outputs: {
resolutionNote: StringColumn({ label: 'Resolution Note', maxLength: 100, order: 100 }),
resolvedBy: StringColumn({ label: 'Resolved By', maxLength: 100, order: 200 }),
},
},
{ triggers: [] },
{
lanes: (params) => {
const lane_0 = wfa.playbook.lane({
config: {
$id: Now.ID['set_outputs_lane'],
label: 'Main Lane',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
activities: () => {
const set_outputs = wfa.playbook.activity(
ActivityDefinitions.Core.SetPlaybookOutputs,
{
$id: Now.ID['act_set_outputs'],
label: 'Set Playbook Outputs',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
},
{
// Keys MUST be output element names declared in `outputs` above.
playbook_outputs: TemplateValue({
resolutionNote: 'Resolved automatically by playbook.', // literal
resolvedBy: wfa.playbook.dataPill(params.parentRecord.assigned_to), // data pill
}),
},
)
return { set_outputs: set_outputs }
},
})
return { lane_0: lane_0 }
},
},
)

Key points:

  • Keys are output element names, not labels, and are case-sensitive. resolutionNote (the outputs key) matches; ResolutionNote would not.
  • Setting an undeclared output is a build error. A playbook_outputs key that is not declared in the playbook's outputs fails the build with '<key>' is not a declared output of this playbook. (Intellisense cannot catch this — TemplateValue accepts any object — so it is enforced at build time.)
  • You never set the dependent table. The backing subflow's second input, playbook_outputs_var_table_name, is auto-derived from the host playbook and is intentionally not part of the DSL.
  • outputs does not require a parentTable. Unlike inputs (which need a parent table for the parentRecord mapper), a standalone playbook with no parentTable can still declare and write outputs.

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).

Start With Delay

Like lanes, a regular activity's ActivityConfig accepts an optional startWithDelay to delay the activity's start after its startRule is satisfied:

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

const remindLater = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_remind_todo'],
label: 'Check Your To-Do List',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
startWithDelay: {
type: 'explicit',
duration: Duration({ hours: 2 }),
timerSchedule: '30b99b3d93a0220050bef157b67ffb2e',
},
},
{ message: 'Reminder: check your to-do list.' },
)

startWithDelay supports the same 'explicit', 'relative', and 'percentage' delay types on activities as it does on lanes — including the optional timerSchedule (a cmn_schedule sys_id or Record<'cmn_schedule'> reference that restricts the delay to the schedule's active hours). See "Start With Delay" in playbook-lanes-guide for the full type breakdown and examples, and startWithDelay in playbook-api for the type signature.

Note that optional activities do not accept startWithDelay — see the config comparison table below.

Optional Activities

An activity can be marked optional: it only runs if a user triggers it during a running playbook, and skipping it doesn't block the rest of the playbook. Use wfa.playbook.run.Manually() as the startRule instead of Immediately() or After(...).

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

const escalateManually = wfa.playbook.activity(
ActivityDefinitions.Core.Instruction,
{
$id: Now.ID['act_manual_escalate'],
label: 'Escalate (Optional)',
startRule: wfa.playbook.run.Manually(),
restartRule: 'RUN_ONLY_ONCE',
},
{ message: 'Escalate this case if needed.' },
)

An optional activity's config is narrower than a regular ActivityConfig:

FieldRegular activityOptional activity
orderRequiredNot accepted — an optional activity has no fixed position
conditionToRunOptionalNot accepted — a person decides whether it runs, not a condition
restartRuleAny RestartRuleMust be 'RUN_ONLY_ONCE'
startRuleImmediately() / After(...)Manually() only
startWithDelayOptionalNot accepted — Playbook Designer hides start-delay configuration for optional activities

Declare it inside a lane for a lane-scoped optional activity (available only while that lane is active), or at the lanes body root (like a stage-level Decision) for a playbook-wide/global optional activity, available for the whole run:

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

const body = {
lanes: () => {
// Global — available for the entire playbook run.
const globalEscalate = wfa.playbook.activity(ActivityDefinitions.Core.Instruction, {
$id: Now.ID['act_global_escalate'],
label: 'Escalate (Optional)',
startRule: wfa.playbook.run.Manually(),
restartRule: 'RUN_ONLY_ONCE',
})

const triage = wfa.playbook.lane({
config: { $id: Now.ID['triage'], label: 'Triage', order: 1, startRule: wfa.playbook.run.Immediately(), restartRule: 'RUN_ONLY_ONCE' },
activities: () => {
// Lane-scoped — available only while the Triage lane is active.
const requestInfo = wfa.playbook.activity(ActivityDefinitions.Core.Instruction, {
$id: Now.ID['act_request_info'],
label: 'Request More Info (Optional)',
startRule: wfa.playbook.run.Manually(),
restartRule: 'RUN_ONLY_ONCE',
})
return { requestInfo: requestInfo }
},
})

return { globalEscalate: globalEscalate, triage: triage }
},
}

An optional activity can't be depended on. Since it may never run, nothing may wait on it or reference it in a pill. Both are compile errors:

// Compile error — an optional activity may never run, so nothing can wait on it.
startRule: wfa.playbook.run.After(globalEscalate)

// Compile error — an optional activity has no referenceable fields.
message: wfa.playbook.dataPill(globalEscalate.outputs.record.state)

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.

AI Agent Configuration

The AI-agent fields on ActivityConfig (enableAiAgent, aiAgentObjective, aiAgentObjAdditionalDetail, aiAgentExecutionMode, aiAgentRunWithRoles, aiAgentSupportedActions, aiAgentRunAs, conversationalAgents) are only available on activity definitions that opt in. See ActivityConfig in playbook-api for the field-by-field type overview — this section covers how to determine, for any given activity definition, whether it supports AI agents, which of these fields it exposes, and how validation/inheritance behaves.

On the platform, AI agent configuration only takes effect when the sn_genai_platform store app is installed and the sn_pa_designer.enable_agentic_playbooks system property is true. The Fluent SDK doesn't validate either of these — the type-level rules and build-time behavior described in this section apply regardless of the target instance's plugin/property state.

Determining AI-agent support for a definition

Check the definition file under src/api/playbook/built-ins/activity-definitions/ for the activity you're using:

  • enableAiAgent: 'on' on the definition is what exposes any AI-agent fields on ActivityConfig at all. If the definition doesn't set it (the default is 'off'), setting any AI-agent field on an activity built from it is a TypeScript error.
  • aiAgentExecutionMode: 'on' as a top-level property of the definition (separate from aiAgentFieldDisplayPreferences — see below) is what makes Autonomous mode available to activity instances. If the definition doesn't set this, instances built from it are restricted to Collaborative mode ('off'); setting aiAgentExecutionMode: 'on' on such an instance is a build error.
  • aiAgentFieldDisplayPreferences on the definition (mirrors inputDisplayPreferences/ experienceDisplayPreferences) controls exactly which AI-agent fields an instance built from it may set. If the definition sets enableAiAgent: 'on' but declares no aiAgentFieldDisplayPreferences, all AI-agent fields are exposed. Otherwise only the fields listed in the map are — setting any other AI-agent field is a TypeScript error, even though the definition otherwise supports AI agents.

Do not infer a definition's AI-agent support or field set from another definition — two AI-capable definitions can differ in whether Autonomous mode is available and in which fields aiAgentFieldDisplayPreferences exposes (e.g. one may omit conversationalAgents or aiAgentObjAdditionalDetail while another includes them).

What's allowed and what isn't

  • aiAgentObjective is required once enableAiAgent is true. If it's omitted, the definition's own default (if any — see below) is used. If it's omitted and the definition has no default, or if it's explicitly set to '', the build errors: `aiAgentObjective` is required when `enableAiAgent` is true. An explicit empty string always overrides the definition's default — it does not fall back silently.
  • aiAgentObjAdditionalDetail only applies in Autonomous mode. If it's set while aiAgentExecutionMode is not 'on', the build warns ( `aiAgentObjAdditionalDetail` only applies when `aiAgentExecutionMode` is `on`; it will be discarded.) and the value is discarded — it is not persisted.
  • aiAgentExecutionMode: 'on' requires the definition to enable Autonomous mode. Check whether the activity definition supports Autonomous mode by verifying that its own aiAgentExecutionMode property is set to 'on' in the definition file (see Determining AI-agent support for a definition above). Setting aiAgentExecutionMode: 'on' on an instance whose definition doesn't support it is a build error: `aiAgentExecutionMode` can only be `on` when the activity definition enables Autonomous mode. Use 'off', or switch to a definition that supports Autonomous mode.
  • aiAgentRunWithRoles and conversationalAgents accept a mix of raw sys_id strings and typed referencesRecord<'sys_user_role'> for roles, Record<'sn_aia_agent'> for conversational agents — rather than only strings.
  • aiAgentSupportedActions only accepts 'update_record', 'create_record', and 'mark_complete'. It lists the actions the AI agent is permitted to take when completing the activity.
  • aiAgentRunAs only accepts 'prior_activity_user' or 'playbook_user'. 'playbook_user' runs the agent as the user who triggered the playbook. 'prior_activity_user' runs it as a specific user selected from a prior activity — that user is held in aiAgentUser (internal, pill-picked), which is only meaningful — and only used by the platform — when aiAgentRunAs is 'prior_activity_user'; it's unused when aiAgentRunAs is 'playbook_user'. When aiAgentRunAs is omitted or nullish, it falls back to the activity definition's own aiAgentRunAs default (see below); if the definition declares none either, it defaults to 'playbook_user'.

Default-value inheritance from the activity definition

A definition can declare its own defaults for aiAgentObjective, aiAgentObjAdditionalDetail, aiAgentRunAs, conversationalAgents, aiAgentRunWithRoles, and aiAgentSupportedActions as plain properties on the ActivityDefinition({...}) call itself (not a separate defaultAiAgent...-style property, unlike defaultInputs/defaultExperienceProperties). When an activity built on such a definition omits one of these fields entirely, the definition's default is used automatically:

// Example: RecordForm declares its own aiAgentObjective/aiAgentRunAs defaults, so omitting
// them here inherits those — check the definition file to see what any given definition declares.
recordForm: wfa.playbook.activity(
ActivityDefinitions.Core.RecordForm,
{
$id: Now.ID['review_record'],
label: 'Review Record',
order: 1,
startRule: wfa.playbook.run.Immediately(),
restartRule: 'RUN_ONLY_ONCE',
enableAiAgent: true,
}
)

An explicit value always takes priority over the definition's default — including an explicit empty string or empty array, which is treated as "no value" rather than "use the default." Only truly omitting the field inherits the definition's default.

aiAgentFieldDisplayPreferences interacts with this default-inheritance mechanism, and it's not the same all-or-nothing gate it is for inputs/experience properties: if a definition excludes one of these AI fields from its aiAgentFieldDisplayPreferences (so it isn't author-facing), setting it explicitly is a TypeScript error, but the definition's own default for it still applies as long as the agent is enabled — falling back to an empty value ('' or []) if the definition declares no default of its own either. This lets a definition bake in a fixed value for a field without exposing it for authors to override.

Check the definition file to see which of these defaults, if any, it actually declares — a definition supporting AI agents doesn't necessarily declare a default for every field eligible for inheritance.

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.