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
- Query
sys_gen_ai_providerfor provider display names - Query
sys_generative_ai_provider_mappingwithexternal=true^active=truefor Provider APIs - Query
sys_generative_ai_model_configfor each mapping withprovider=<mapping_sys_id>^active=true
Key Field Mappings
| What You Need | Table | Column | Code Property |
|---|---|---|---|
| Provider name | sys_generative_ai_provider_mapping | provider (display) | provider key |
| Provider API flow | sys_generative_ai_provider_mapping | provider_implementation | providerAPI.id |
| Model identifier | sys_generative_ai_model_config | model | model |
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
| Property | Required | Description |
|---|---|---|
$id | Yes | Unique identifier |
name | Yes | Attribute name (must not contain underscores or special characters) |
description | No | Description of the input attribute |
dataType | Yes | 'string', 'numeric', 'boolean', 'glide_record', 'simple_array', 'json_object', 'json_array' |
mandatory | No | Whether input is required (default: false) |
truncate | No | Whether to truncate the value (only valid for: string, numeric, boolean, glide_record; NOT supported for: simple_array, json_object, json_array) |
testValues | No | Values for testing and validation |
tableName | Conditional | Required when using glide_record dataType |
tableSysId | Conditional | Required 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
| Method | Purpose |
|---|---|
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 Type | Access 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 | $capabilityId | output.$id | Per-input $id | Per-output $id |
|---|---|---|---|---|---|
t.InlineScript() | Yes | No | No | No | No |
t.Script() | Yes | Yes | Yes | Yes | No |
t.WebSearch() | Yes | No | No | No | No |
t.Skill() | Yes | No | No | No | No |
t.FlowAction() | Yes | Yes | No | Yes | Yes |
t.Subflow() | Yes | Yes | No | Yes | Yes |
t.Decision() | Yes | No | No | No | No |
$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 tot.Script().- Per-input / per-output
$id: Each entry in theinputsoroutputsarray 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
| Property | Required | Description |
|---|---|---|
$id | Yes | Unique identifier |
model | Yes | Always 'llm_generic_small_v2' for Now LLM Service |
promptState | Yes | Always 'draft' |
prompt | Yes | Text or builder function (p) => string |
temperature | No | 0.0-1.0 (default: 0.2) |
maxTokens | No | Required for non-Now LLM providers |
filterCondition | No | Per-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 Type | Recommended maxTokens |
|---|---|
| Short answers (yes/no, category) | 100-200 |
| Brief summaries | 300-500 |
| Formatted reports | 1000-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
| Type | Configuration | Use 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.
| Field | Accepts | Maps to | Use when |
|---|---|---|---|
roleRestrictions | role sys_ids as strings, Role objects, or DbRecord<'sys_user_role'> references | sys_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 |
roleMap | role names as strings, Role() objects, or DbRecord<'sys_user_role'> references | sys_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. UseroleMapfor name-based authoring, or pass aRole/DbRecord<'sys_user_role'>reference which resolves to a sys_id at build.roleMap— plain string sys_ids produce a build-time diagnostic. UseroleRestrictionsfor sys_id-based authoring, or pass aRole/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 inrole_list. No mapping rows.roleMap→ onesys_agent_access_role_mappingrow per role, with the role reference carrying the name as a coalesce key.role_listleft 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.
Example 1 — name-based (recommended for ZP10+)
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
| Channel | Configuration | When to Use |
|---|---|---|
| UI Action | uiAction: { $id: Now.ID['skill_uiaction'], table: 'incident' } | Button on record form |
| Now Assist Panel | nowAssistPanel: { enabled: true, roles: ['now_assist_panel_user'] } | Chat interface |
| Flow Action | flowAction: true | Flow Designer integration |
| None | Omit deploymentSettings | Programmatic 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:
| Type | Parse |
|---|---|
| Number | parseInt(input, 10) |
| JSON | JSON.parse(input) in try-catch |
| Boolean | input === '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
| Setting | Value |
|---|---|
| Pattern | Class.create() |
clientCallable | false |
accessibleFrom | public |
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
- Read the existing
.now.tsfile insrc/fluent/now-assist-skills/ - Identify what needs to change
- Preserve unchanged configuration
- Update
testValuesif inputs changed - Build, install, and verify
Change Impact Matrix
| Change | Also Update |
|---|---|
| Add input | Update testValues, update prompts to reference new input |
| Remove input | Remove all prompt references, check tool dependencies |
| Add tool | Update prompts, add to return statement in tools callback |
| Change provider/model | Verify availability, may need prompt adjustments |
Change userAccess | If switching to roles, provide role sys_ids |
Validation Rules
Required Fields
| Rule | Description |
|---|---|
$id required | Unique identifier for the skill |
name required | Skill display name |
securityControls required | Must include userAccess and roleRestrictions |
providers required | At least one provider with prompts |
promptState: 'draft' | Always use 'draft' -- other values cause validation errors |
Valid Enums
| Property | Valid 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
| Wrong | Correct |
|---|---|
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 inputs | Only ONE allowed |
Troubleshooting
| Problem | Solution |
|---|---|
| Prompt variable not replaced | Check name conversion (spaces to underscores) |
| Tool output not available | Verify tool executed before prompt references it |
| Script execution failed | Check context.getValue() uses snake_case |
| Skill name too long | Shorten to 40 characters |
| Missing glide_record fields | Provide both tableName and tableSysId |
| ShorthandPropertyAssignment | Use { tool: tool } not { tool } |
| UI Action not appearing | Verify table name, check record exists |
| Skill not in NAP | Set 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>/