Skip to main content
Version: 4.7.0

Now Assist Skills Guide

Create and configure Now Assist Skills using the NowAssistSkillConfig API in the Fluent SDK. Skills use configured prompts to generate AI responses from an LLM, optionally enriched by inputs and tools (script, inline script, web search, subflows, flow actions, decision branches). This guide covers the full lifecycle: provider/model selection, input configuration, tool wiring, prompt authoring, security controls, and deployment. Requires SDK 4.6.0 or higher.


When to Use

  • Creating or modifying Now Assist Skills using NowAssistSkillConfig
  • Adding or modifying skill prompts, inputs, or security controls
  • Adding or modifying skill tools (script, inline script, web search, subflows, flow actions, skill-as-tool, decision branches)
  • Configuring skill deployment to UI Actions, Flow Actions, or Now Assist Panel

Skill Structure

NowAssistSkillConfig(Arg 1: SkillDefinition, Arg 2: SkillPromptConfig)

API Structure

import { NowAssistSkillConfig } from '@servicenow/sdk/core'

NowAssistSkillConfig(
// Arg 1: SkillDefinition
{
$id: Now.ID['skill_name'],
name: string,
description?: string,
shortDescription?: string,
inputs?: InputAttribute[],
outputs?: OutputAttribute[], // optional — 5 standard outputs auto-generated if omitted
tools?: (t: ToolGraphBuilder) => ToolHandles | void,
securityControls: SecurityControls, // MANDATORY
skillSettings?: SkillSettings,
deploymentSettings?: DeploymentSettings
},
// Arg 2: SkillPromptConfig
{
providers: [{
provider: 'Now LLM Service' | 'Azure OpenAI' | 'Open AI' | string,
providerAPI?: { type: 'sys_hub_flow', id: string },
prompts: [{
name: string,
versions: [{
$id: Now.ID['prompt_v1'],
model: string,
temperature?: number,
maxTokens?: number,
promptState: 'draft',
prompt: string | ((p: PromptBuilder) => string),
filterCondition?: { [inputName: string]: string }
}]
}]
}]
}
)

Provider and Model Configuration

The GenAI configuration follows a 3-level hierarchy: Provider -> Provider API -> Model.

Query Steps

  1. Query sys_gen_ai_provider for provider display names
  2. Query sys_generative_ai_provider_mapping with external=true^active=true for Provider APIs
  3. Query sys_generative_ai_model_config for each mapping with provider=<mapping_sys_id>^active=true

Key Field Mappings

What You NeedTableColumnCode Property
Provider namesys_generative_ai_provider_mappingprovider (display)provider key
Provider API flowsys_generative_ai_provider_mappingprovider_implementationproviderAPI.id
Model identifiersys_generative_ai_model_configmodelmodel

Default Recommendation

Use Now LLM Service with Now LLM Generic provider API and llm_generic_small_v2 model.

Code Example

providers: [{
provider: 'Now LLM Service',
providerAPI: {
type: 'sys_hub_flow',
id: '<provider_implementation_sys_id>'
},
prompts: p => ({
model: 'llm_generic_small_v2',
// prompt config
})
}]

Known Providers

'Now LLM Service', 'Azure OpenAI', 'Open AI', 'Google Gemini', 'AWS Claude', 'IBM Watson', 'Perplexity', 'Aleph Alpha', 'Custom LLM Provider'


Input Attributes

PropertyRequiredDescription
$idYesUnique identifier
nameYesAttribute name (must not contain underscores or special characters)
descriptionNoDescription of the input attribute
dataTypeYes'string', 'numeric', 'boolean', 'glide_record', 'simple_array', 'json_object', 'json_array'
mandatoryNoWhether input is required (default: false)
truncateNoWhether to truncate the value (only valid for: string, numeric, boolean, glide_record; NOT supported for: simple_array, json_object, json_array)
testValuesNoValues for testing and validation
tableNameConditionalRequired when using glide_record dataType
tableSysIdConditionalRequired when testValues is provided for glide_record type

Constraint: A skill can have at most one input of type glide_record.

Name conversion: "Incident Number" becomes {{incident_number}} in prompts.

glide_record vs Tools

Use glide_record input (preferred) when:

  • Single table query
  • No calculations or transformations needed
  • No tools need access to the record data

Use tools with string input type when:

  • Multiple tables or joins needed
  • Calculations or transformations required
  • Record data needed inside a tool script

Critical: glide_record inputs are NOT accessible inside tools. They can only be referenced in prompts via p.input.recordName.fieldName.


Output Attributes

Do NOT define custom outputs. The platform automatically creates five default outputs: response, confidence, explanation, metadata, citations. Reference them in prompts with ${p.output.response}.


Tool Configuration

Tool Methods

MethodPurpose
t.Script()Reference Script Include
t.InlineScript()Inline script function
t.WebSearch()Web search (AI answers)
t.Skill()Call another skill
t.Subflow()Execute Flow Designer subflow
t.FlowAction()Execute Flow Designer action
t.Decision()Conditional branching

Output Access in Prompts

Tool TypeAccess Pattern
t.Script() / t.InlineScript()${p.tool.ToolName.output}
t.WebSearch() / t.Skill()${p.tool.ToolName.response}
t.FlowAction() / t.Subflow()${p.tool.ToolName.outputName}

Required Identifiers

Every tool needs a $id. Some tools need additional identifiers depending on how they integrate with the platform:

Tool Type$id$capabilityIdoutput.$idPer-input $idPer-output $id
t.InlineScript()YesNoNoNoNo
t.Script()YesYesYesYesNo
t.WebSearch()YesNoNoNoNo
t.Skill()YesNoNoNoNo
t.FlowAction()YesYesNoYesYes
t.Subflow()YesYesNoYesYes
t.Decision()YesNoNoNoNo
  • $id: Unique record identifier for the tool itself.
  • $capabilityId: Identifies the external capability (Script Include, Flow Action, or Subflow) the tool wraps. Required because these tools reference a platform artifact that exists outside the skill definition.
  • output.$id: Script tools produce a custom output attribute that needs its own identifier. Only applies to t.Script().
  • Per-input / per-output $id: Each entry in the inputs or outputs array is a separate mapping record and needs its own unique $id.

InlineScript Tool

const getIncident = t.InlineScript("GetIncident", {
$id: Now.ID["skill_getincident_tool"],
script: `(function(context) {
var gr = new GlideRecord('incident');
gr.addQuery('number', context.getValue('incident_number'));
gr.query();
if (gr.next()) {
return {
number: gr.getValue('number'),
short_description: gr.getValue('short_description')
};
}
return { error: 'Incident not found' };
})(context)`
});

Access inputs via context.getValue('input_name') with snake_case conversion.

Script Tool

Requires an external Script Include with accessibleFrom: 'all'. Each inputs[].name must match a function parameter name exactly.

const myTool = t.Script("MyTool", {
$id: Now.ID["my_skill_script_tool"],
$capabilityId: Now.ID["my_skill_script_capability"],
output: { $id: Now.ID["my_skill_script_output"] },
scriptId: myScriptInclude,
scriptFunctionName: 'processData',
inputs: [
{ $id: Now.ID["tool_input_id"], name: 'param', value: t.input.description }
]
});

value only accepts: t.input.inputName, "hardcoded string", or previousTool.output.

WebSearch Tool

const searchWeb = t.WebSearch("SearchWeb", {
$id: Now.ID["my_skill_search_tool"],
searchType: "ai_answers",
query: t.input.userQuery,
aiSearchProviders: "perplexity" // optional
});

Always use searchType: 'ai_answers'. External providers require API key configuration.

Decision Node

const checkPriority = t.Decision("CheckPriority", {
$id: Now.ID["skill_decision"],
depends: [getData],
targets: ["Escalate", "Standard"] as const,
branches: targets => [{
name: "Critical",
to: targets.Escalate,
condition: { field: getData.priority, operator: "is", value: "1" }
}],
default: targets => targets.Standard
});

Branch conditions: { field, operator, value } where operator is 'is' or 'is_not', and value is a plain string.

Tool Chaining

Use the depends property:

const searchWeb = t.WebSearch("SearchWeb", {
$id: Now.ID["search_tool"],
searchType: "ai_answers",
query: getIncident.output,
depends: [getIncident]
});

Prompt Configuration

Mandatory 4-Section Structure

## Role
You are a [specialist type]. Your task is to [objective].

## Context
The user's [input]: '{{input_name}}'
Tool output: '${p.tool.ToolName.output}'

## Instructions
1. [First step]
2. [Second step]
3. [Constraints]

## Output
The output should be [format]. It must be [qualities].

Prompt Versions

PropertyRequiredDescription
$idYesUnique identifier
modelYesAlways 'llm_generic_small_v2' for Now LLM Service
promptStateYesAlways 'draft'
promptYesText or builder function (p) => string
temperatureNo0.0-1.0 (default: 0.2)
maxTokensNoRequired for non-Now LLM providers
filterConditionNoPer-version usage conditions

Type-Safe Prompt Builder

prompt: p => `## Role
You are an incident specialist.

## Context
Incident details: ${p.tool.GetIncident.output}

## Instructions
1. Review the incident data.
2. Provide actionable recommendations.

## Output
Provide a structured analysis with Summary, Root Cause, and Recommendations.`

maxTokens Guidelines

Output TypeRecommended maxTokens
Short answers (yes/no, category)100-200
Brief summaries300-500
Formatted reports1000-2000

Warning: Setting maxTokens too low for formatted output forces raw JSON responses.

Conditional Prompts (filterCondition)

versions: [
{
$id: Now.ID["prompt_high"],
model: "llm_generic_small_v2",
promptState: "draft",
prompt: p => `High priority analysis: ${p.input.description}`,
filterCondition: { priority: "1" }
},
{
$id: Now.ID["prompt_default"],
model: "llm_generic_small_v2",
promptState: "draft",
prompt: p => `Standard analysis: ${p.input.description}`
}
]

Keys must exactly match input attribute names (case-sensitive, including spaces).


Security Controls (Mandatory)

userAccess

TypeConfigurationUse Case
Authenticated{ $id: Now.ID['ua'], type: 'authenticated' }General, non-sensitive
Role-Based{ $id: Now.ID['ua'], type: 'roles', roles: ['itil'] }Sensitive data

Role configuration: roleRestrictions and roleMap

The skill can declare which roles it inherits at execution time via two independent fields. At least one of them must be non-empty.

FieldAcceptsMaps toUse when
roleRestrictionsrole sys_ids as strings, Role objects, or DbRecord<'sys_user_role'> referencessys_agent_access_role_configuration.role_list (legacy glide_list column)Targeting pre-ZP10 / pre-AP3 customer instances, OR all customer instances are cloned from a common production baseline so sys_ids match
roleMaprole names as strings, Role() objects, or DbRecord<'sys_user_role'> referencessys_agent_access_role_mapping (new M2M reference table)Targeting ZP10 / AP3 or later — strongly preferred because the platform resolves each name to the correct sys_id on every target instance, surviving independently provisioned instances

The maint role is NOT allowed in either field.

Validation (build-time diagnostics, not TypeScript compile errors):

  • roleRestrictions — plain string role names (e.g., 'admin') produce a build-time diagnostic. Use roleMap for name-based authoring, or pass a Role / DbRecord<'sys_user_role'> reference which resolves to a sys_id at build.
  • roleMap — plain string sys_ids produce a build-time diagnostic. Use roleRestrictions for sys_id-based authoring, or pass a Role / DbRecord<'sys_user_role'> reference.
  • If BOTH are empty/absent the build fails with a clear diagnostic.

Build emission per field:

  • roleRestrictions → comma-joined sys_ids in role_list. No mapping rows.
  • roleMap → one sys_agent_access_role_mapping row per role, with the role reference carrying the name as a coalesce key. role_list left empty.

If you populate both fields, the instance gets both storages populated. The platform DAO unions and dedupes at runtime, so the same role doesn't get enforced twice.

securityControls: {
userAccess: {
$id: Now.ID['skill_user_access'],
type: 'authenticated',
},
roleMap: ['admin', 'itil'],
}

Example 2 — custom role defined by this app

import { Role } from '@servicenow/sdk/core'

const viewer = Role({
name: 'x_snc_myapp.viewer',
description: 'Read-only viewer for my app',
grantable: true,
})

NowAssistSkillConfig({
...
securityControls: {
userAccess: { $id: Now.ID['ua'], type: 'authenticated' },
roleMap: ['admin', viewer], // mix string names and Role() objects
},
...
})

Build emits the sys_user_role record for viewer AND a mapping row referencing it — both deploy as part of the same app.

Example 3 — DbRecord<'sys_user_role'> reference

import { Record } from '@servicenow/sdk/core'

// Reference a custom app-scoped role by name using Record.
const customRoleRef = Record({
$id: Now.ID['custom_role_ref'],
table: 'sys_user_role',
data: { name: 'x_snc_myapp.skill_user' },
})

NowAssistSkillConfig({
...
securityControls: {
userAccess: { $id: Now.ID['ua'], type: 'authenticated' },
roleMap: [customRoleRef], // typed reference to a custom role by name
},
...
})

DbRecord<'sys_user_role'> is useful when you want to declare role references with explicit typing. The data schema only exposes the role's settable columns (name, description, grantable, etc.) — sys_id is not in the schema, so the type system prevents passing a sys_id-only reference. For sys_id-based references use roleRestrictions; for existing platform roles like admin/itil/custom-role, declare the providing app as a dependency and use string names in roleMap.

Example 4 — sys_id-based (legacy / pre-ZP10)

securityControls: {
userAccess: {
$id: Now.ID['skill_user_access'],
type: 'roles',
roles: ['itil']
},
roleRestrictions: ['282bf1fac6112285017366cb5f867469']
}

Example 5 — both fields during a migration window

securityControls: {
userAccess: { $id: Now.ID['ua'], type: 'authenticated' },
// role_list path (works on every instance, even pre-ZP10)
roleRestrictions: ['2831a114c611228501d4ea6c309d626d'],
// mapping-table path (works cross-instance on ZP10+ via name coalesce)
roleMap: ['itil'],
}

Deployment Configuration

ChannelConfigurationWhen to Use
UI ActionuiAction: { $id: Now.ID['skill_uiaction'], table: 'incident' }Button on record form
Now Assist PanelnowAssistPanel: { enabled: true, roles: ['now_assist_panel_user'] }Chat interface
Flow ActionflowAction: trueFlow Designer integration
NoneOmit deploymentSettingsProgrammatic only
deploymentSettings: {
uiAction: { $id: Now.ID['skill_uiaction'], table: 'incident' },
nowAssistPanel: {
enabled: true,
roles: ['now_assist_panel_user']
},
flowAction: true,
skillFamily: 'aabb0011ccdd2233eeff4455aabb6677' // optional
}

Skill Settings (Pre/Post Processors)

skillSettings: {
providers: [{
$id: Now.ID['skill_preprocessor'],
name: 'TestPreProcessor',
preprocessor: `(function(payload) {
payload.timestamp = new Date().toISOString();
return payload;
})(payload);`,
},
{
$id: Now.ID['skill_postprocessor'],
name: 'TestPostProcessor',
postprocessor: `(function(payload) {
if (payload.response) {
payload.formatted = payload.response.trim();
}
return payload;
})(payload);`
}]
}

Script Guidelines

Glide APIs Are Global

gs, GlideRecord, GlideAggregate, GlideDateTime are globally available. Never import them.

Input Parsing

All tool inputs arrive as strings:

TypeParse
NumberparseInt(input, 10)
JSONJSON.parse(input) in try-catch
Booleaninput === 'true'

Context Access

  • TypeScript tool callbacks: context.inputs["incident number"] (exact name, bracket notation)
  • GlideRecord scripts: context.getValue('incident_number') (snake_case)

Script Include Requirements

SettingValue
PatternClass.create()
clientCallablefalse
accessibleFrompublic

Complete Example

import { NowAssistSkillConfig } from "@servicenow/sdk/core";

export const incidentAnalyzer = NowAssistSkillConfig(
{
$id: Now.ID["incident_analyzer"],
name: "Incident Analyzer",
inputs: [
{
$id: Now.ID["incident_number"],
name: "incident number",
mandatory: true,
dataType: "string",
testValues: "INC0010001"
}
],
tools: t => {
const getIncident = t.InlineScript("GetIncident", {
$id: Now.ID["analyzer_getincident_tool"],
script: `(function(context) {
var gr = new GlideRecord('incident');
gr.addQuery('number', context.getValue('incident_number'));
gr.query();
if (gr.next()) {
return {
number: gr.getValue('number'),
short_description: gr.getValue('short_description'),
priority: gr.getValue('priority')
};
}
return { error: 'Incident not found' };
})(context)`
});
return { GetIncident: getIncident };
},
securityControls: {
userAccess: {
$id: Now.ID["analyzer_user_access"],
type: "roles",
roles: ["itil"]
},
roleRestrictions: ["<ROLE_SYS_ID>"]
},
deploymentSettings: {
uiAction: { $id: Now.ID["analyzer_uiaction"], table: "incident" },
nowAssistPanel: {
enabled: true,
roles: ["now_assist_panel_user"]
}
}
},
{
providers: [{
provider: "Now LLM Service",
prompts: [{
name: "Analyze",
versions: [{
$id: Now.ID["analyzer_prompt_v1"],
model: "llm_generic_small_v2",
promptState: "draft",
prompt: p => `## Role
You are an incident analysis specialist.

## Context
Incident details: ${p.tool.GetIncident.output}

## Instructions
1. Review the incident number, description, and priority.
2. Identify root cause based on available information.
3. Provide actionable recommendations.

## Output
**Summary:** [Brief overview]
**Root Cause:** [Identified cause or 'Requires investigation']
**Recommendations:** [Numbered action items]`
}]
}]
}]
}
);

Editing Existing Skills

Edit Workflow

  1. Read the existing .now.ts file in src/fluent/now-assist-skills/
  2. Identify what needs to change
  3. Preserve unchanged configuration
  4. Update testValues if inputs changed
  5. Build, install, and verify

Change Impact Matrix

ChangeAlso Update
Add inputUpdate testValues, update prompts to reference new input
Remove inputRemove all prompt references, check tool dependencies
Add toolUpdate prompts, add to return statement in tools callback
Change provider/modelVerify availability, may need prompt adjustments
Change userAccessIf switching to roles, provide role sys_ids

Validation Rules

Required Fields

RuleDescription
$id requiredUnique identifier for the skill
name requiredSkill display name
securityControls requiredMust include userAccess and roleRestrictions
providers requiredAt least one provider with prompts
promptState: 'draft'Always use 'draft' -- other values cause validation errors

Valid Enums

PropertyValid Values
dataType'string', 'numeric', 'boolean', 'glide_record', 'simple_array', 'json_object', 'json_array'
promptState'draft', 'finalized', 'published', 'archived'
userAccess.type'authenticated', 'roles'
providerAPI.type'sys_hub_flow', 'sys_hub_action_type_definition', 'sys_script_include', 'one_api_system_executor'

Common Hallucinations to Avoid

WrongCorrect
promptState: 'published'promptState: 'draft'
dataType: 'record'dataType: 'glide_record'
dataType: 'text'dataType: 'string'
roleRestrictions: ['admin']roleRestrictions: ['<sys_id>']
${p.tool.WebSearch.output}${p.tool.WebSearch.response}
Multiple glide_record inputsOnly ONE allowed

Troubleshooting

ProblemSolution
Prompt variable not replacedCheck name conversion (spaces to underscores)
Tool output not availableVerify tool executed before prompt references it
Script execution failedCheck context.getValue() uses snake_case
Skill name too longShorten to 40 characters
Missing glide_record fieldsProvide both tableName and tableSysId
ShorthandPropertyAssignmentUse { tool: tool } not { tool }
UI Action not appearingVerify table name, check record exists
Skill not in NAPSet nowAssistPanel: { enabled: true, roles: ['now_assist_panel_user'] }, publish in platform

Post-Creation: Skill URL

After creating a skill, query sn_nowassist_skill_config for skill_id and construct: https://<instance_name>/now/now-assist-skillkit/skill/<skill_id>/