We're reclaiming inactive PDIs to keep them available for active builders. Learn what's changing, who's affected, and how to protect your work. Read More

Earl Duque
Administrator

Everything AI in ServiceNow runs on skills: prompts, AI Agents, and analysis skills that live inside the Now Platform and have native access to your data, your tables, and your security model. Normally you invoke them from inside the platform: a UI Action, a Flow, a Virtual Agent topic.

 

But what if the automation starts outside ServiceNow? What if you want an external app, a middleware layer, or a scheduled job somewhere else to reach into your instance, fire an AI Skill, and get the result back, all over a plain REST call?

 

That is exactly what this walkthrough covers. We will build an AI Skill from scratch in Now Assist Skill Kit, then expose it two different ways so you can call it from anywhere that can send an HTTP request: a Scripted REST API (the synchronous method) and the Data Broker endpoints (the platform’s own asynchronous, two-call pattern). Every script below is templatized and copy-pastable, with notes on exactly what to replace.

 

calling ai skills.png

 

Table of Contents

 

  1. What we’re building
  2. Prerequisites
  3. Step 1: Build the AI Skill in Now Assist Skill Kit
    1. Create the skill and prompt
    2. Add an input
    3. Write and test the prompt
    4. Deploy the skill (and generate the script)
  4. Step 2: Find your capability ID and skill config ID
  5. Method A: Scripted REST API (recommended)
    1. Create the Scripted REST API
    2. Create a GET resource
    3. The resource script (templatized)
    4. Security and access
    5. Call it from Postman
    6. Parse the response
  6. Method B: Data Broker (advanced, asynchronous)
    1. How the two-call pattern works
    2. Call 1: Execute the skill
    3. Call 2: Retrieve the result via batch
  7. Debugging: the Generative AI Log table
  8. Watch the livestreams
  9. Wrap-up

 

What we’re building

 

We want a repeatable pattern: an AI Skill that lives in your instance and can be called from off-platform via REST, returning its result to whatever sent the request.

 

The example skill we’ll build recommends a travel destination for architecture lovers based on something the caller likes (“skyscrapers,” “mixed-use buildings,” etc.). The skill itself is basic by design. The plumbing around it is the reusable part, and it works identically for any prompt, AI Agent, or analysis skill you build.

 

Here is the shape of what we’re connecting:

 

External caller (Postman, middleware, another app)
        |
        |  HTTP request (REST)
        v
ServiceNow instance
        |
        |-- Method A: Scripted REST API  --> sn_one_extend.OneExtendUtil.executeSecure()  (synchronous, 1 call)
        |
        |-- Method B: Data Broker /exec + /batch                                          (asynchronous, 2 calls)
        v
   AI Skill runs (with full access to instance data + security)
        |
        v
   Result returned to the external caller

 

Both methods drive the same underlying skill execution. I recommend the Scripted REST API for most cases: it’s synchronous, it’s a single call, and you control the exact response shape. The Data Broker method is what the platform uses internally, so it’s worth understanding, though it takes two calls and runs asynchronously.

 

↑ Return to Table of Contents

 

Prerequisites

 

  • An instance with Now Assist Skill Kit (NASK) available and generative AI configured.
  • The sn_skill_builder role, which grants access to Now Assist Skill Kit. Without it, the Skill Kit navigation won’t appear.
  • A user account you can authenticate with over REST (this walkthrough uses Basic Auth with a dedicated api_user). The skill runs as that user, so it inherits that user’s data access and ACLs, which is exactly the benefit.
  • An API client for testing. I’m using Postman.

 

Tip: role changes are session-based. Your list of roles is built once at the start of your session. After you grant yourself sn_skill_builder, log out and back in (or impersonate and un-impersonate) so the new role actually takes effect.

 

↑ Return to Table of Contents

 

Step 1: Build the AI Skill in Now Assist Skill Kit

 

We need a skill first. If you already have one you want to expose, you can skip ahead to Step 2, since the REST plumbing works against any skill.

 

Create the skill and prompt

 

  1. In the navigation, open Now Assist Skill Kit.
  2. Create a new skill. We’ll write ours from scratch and jump straight into the prompt editor.
  3. Name it something meaningful (ours is Recommend travel destination for architectural sightseeing).

 

Add an input

 

Add a single input of type string. We’ll call it something_you_like and leave it non-mandatory to keep things simple.

 

Remember this input name, because it becomes the key your REST payload has to populate. Getting the input name right is one of the two most common reasons a call “succeeds” but returns nothing (the skill runs with an undefined input).

 

Write and test the prompt

 

In the prompt editor, write your instructions and insert the input where you want the model to read it. Here is the exact prompt we used. The {{something_you_like}} token is the input variable inserted from the editor:

 

Given the input below, recommend a travel destination that has good
architectural background or tours or history that a person would be
interested in learning about regarding this thing that they like. If
there is nothing provided below then just make a generic recommendation
based off of a ServiceNow Audience

Input: {{something_you_like}}

 

Run a quick test right in the editor. Give it something like vertical rights for land usage or skyscrapers and confirm you get a sensible recommendation back. Once the skill produces a result on demand, it’s ready to deploy.

 

Deploy the skill (and generate the script)

 

Now go to the skill’s deployment / skill settings. You’ll see several deployment targets: Analysis panel, UI Action, Flow action, Context menu for VA, UI Builder.

 

We’re going to target UI Action for a specific reason: the UI Action deployment auto-generates a server script that calls your skill, and that script is 90% of what we need for the Scripted REST API. It lets the platform write the harder part for us.

 

  1. Choose the UI Action target.
  2. A UI Action needs a table, and it doesn’t matter which one for our purposes since we only want the generated script. Pick any table (we used incident).
  3. Finalize and publish the skill, then activate it (Now Assist Skill Kit → admin > skills). Some skills require a small activation JSON, so accept the default.
  4. Open the generated UI Action and copy its script. It’ll look like the template in the next section.

 

↑ Return to Table of Contents

 

Step 2: Find your capability ID and skill config ID

 

Every method below needs two identifiers that are unique to your skill:

 

  • Capability ID: the sys_id of the One Extend capability record backing your skill.
  • Skill Config ID: the sys_id of your skill’s configuration record.

 

The UI Action script the platform generated for you already contains both, hard-coded. If you copied that script, you can lift the values straight out of it: look for capabilityId and skillConfigId in the generated code. That’s the fastest way to get them without hunting through records.

 

If you ever need to find them manually, they’re the sys_ids of the One Extend capability record and the skill config record you just created. Deploying to a UI Action first is simply the shortcut that surfaces them for you.

 

↑ Return to Table of Contents

 

Method A: Scripted REST API (recommended)

 

This method uses a single synchronous GET backed by one script, and you control the exact response. The skill returns its answer in the same call, with no polling.

 

Create the Scripted REST API

 

  1. Navigate to System Web Services → Scripted REST APIs and click New.
  2. Give it a Name and an API ID. The API ID becomes part of your URL. We used API ID archrecs under the namespace snc.
  3. Save.

 

Your base path will be /api/<namespace>/<api_id>, which for us is /api/snc/archrecs.

 

Create a GET resource

 

  1. On your Scripted REST API record, add a new Resource.
  2. Set the HTTP method to GET.
  3. Leave the relative path as the default (/) so the full endpoint is just the base path.
  4. Drop in the script from the next section.

 

We’re using GET with a query parameter so it’s easy to call: ?user_input=skyscrapers. You could also accept a POST body instead.

 

The resource script (templatized)

 

Here is the full script. It reads the caller’s query parameter, maps it to the skill’s input, executes the skill synchronously through One Extend, pulls the skill’s response out of the result, and returns it as JSON.

 

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

    var inputsPayload = {};
    // Map the incoming query param to your skill's input name.
    // LEFT side  = your skill input name (here: 'something_you_like')
    // RIGHT side = the query param the caller sends (here: 'user_input')
    inputsPayload['something_you_like'] = request.queryParams['user_input'];

    // Paste YOUR capability ID (from the generated UI Action script).
    var capabilityId = '{CAPABILITY_ID}';

    var nask_request = {
        executionRequests: [{
            payload: inputsPayload,
            capabilityId: capabilityId,
            meta: {
                // Paste YOUR skill config ID (from the generated UI Action script).
                skillConfigId: '{SKILL_CONFIG_ID}'
            }
        }],
        mode: 'sync'
    };

    try {
        var nask_response = sn_one_extend.OneExtendUtil.executeSecure(nask_request) || {};
        var skillResponse = ((nask_response["capabilities"] || {})[capabilityId] || {})["response"];
        response.setContentType('application/json');
        var response_body = skillResponse;
        response.setStatus(200);
        return response_body;
    } catch (e) {
        gs.error(e);
        gs.addErrorMessage('Something went wrong while executing skill.');
    }

})(request, response);

 

What to replace:

 

  • {CAPABILITY_ID}: your skill’s capability ID from Step 2.
  • {SKILL_CONFIG_ID}: your skill’s skill config ID from Step 2.
  • 'something_you_like': change this to your skill’s input name. If your skill has multiple inputs, add a line per input on the inputsPayload object.
  • 'user_input': the query parameter name callers will send. Keep it or rename it; just make sure your callers match.

 

Two things about why this works:

 

  • mode: 'sync' is what makes this a single call. The skill runs and returns in-line. (This is what lets us avoid the two-call Data Broker pattern.)
  • We return a plain response body object instead of using a stream writer, and the framework serializes it to JSON for us. That’s simpler and less error-prone.

 

The two lines building inputsPayload and the capabilityId/skillConfigId are lifted directly from the UI Action the Skill Kit generated for you. That’s why we deployed to a UI Action first.

 

Security and access

 

On the resource, review the security settings before you go live:

 

  • Keep “Requires authentication” on. Do not leave the resource open to unauthenticated callers, since you’re handing out access to a generative AI capability that runs as a real user.
  • The skill executes with the permissions of the authenticating user. Use a dedicated integration user (we used api_user) scoped to exactly the data it should touch.
  • Set an appropriate ACL / required role for the resource so only intended callers can reach it.

 

Call it from Postman

 

From any REST client, make a GET request:

 

GET https://{YOUR_INSTANCE}.service-now.com/api/snc/archrecs?user_input=skyscrapers

Authorization: Basic (username: api_user, password: {YOUR_PASSWORD})
Content-Type: application/json

 

What to replace:

 

  • {YOUR_INSTANCE}: your instance name.
  • snc/archrecs: your namespace and API ID if you named them differently.
  • user_input=skyscrapers: your query param name and whatever value you want the skill to reason about.
  • Basic Auth credentials for your integration user.

 

Because the skill runs synchronously, it may take a moment (a generative call is happening on the other end), then returns 200 with the result. If it comes back instantly with an empty object, that’s your signal the input name didn’t line up, so jump to Debugging.

 

Parse the response

 

The response comes back as a JSON object. The actual model output is delivered as a stringified JSON value inside the result property. So on the consuming side you access .result, then JSON.parse() it, then read the model output field. Conceptually:

 

// 'body' is the parsed response from the call
var payload = JSON.parse(body.result);
var answer  = payload.modelOutput;   // the human-readable recommendation

// e.g. "For someone interested in skyscrapers, Chicago is an ideal destination..."

 

That’s the entire synchronous method, and it’s the approach I’d reach for in almost every real integration.

 

↑ Return to Table of Contents

 

Method B: Data Broker (advanced, asynchronous)

 

The Data Broker method is how ServiceNow’s own UIs drive skills internally. You don’t need to write any server-side script for it (you call the platform’s Data Broker endpoints directly), but it’s a two-call, asynchronous pattern with more to manage. Reach for this when you specifically want async behavior (fire the skill, acknowledge immediately, come back for the result), or when you can’t deploy a Scripted REST API.

 

How I reverse-engineered these payloads: run the skill’s test panel inside Now Assist Skill Kit with your browser’s dev tools open (F12) on the Network tab. You’ll see the platform fire the /exec call and then a follow-up /batch call. Copy those request bodies; they are exactly the shapes below. The Network tab must be open before you run the test; it doesn’t record retroactively.

 

How the two-call pattern works

 

  1. Call 1 (Execute): POST to the Data Broker /exec endpoint. This kicks off the skill asynchronously and returns identifiers for the running job (a batch run ID and result IDs). The answer itself comes later.
  2. Call 2 (Retrieve): POST to the /batch endpoint using those IDs to fetch the actual result once the skill has finished. In a real integration you’d poll this (a short wait-loop) until the status is successful.

 

Call 1: Execute the skill

 

POST https://{YOUR_INSTANCE}.service-now.com/api/now/uxf/databroker/exec

Authorization: Basic (username: api_user, password: {YOUR_PASSWORD})
Content-Type: application/json

 

The body is an array (the Data Broker accepts multiple requests at once), and the skill parameters are a JSON string inside inputValues.payload.value:

 

[
  {
    "type": "TRANSFORM",
    "definitionSysId": "{DEFINITION_SYS_ID}",
    "parentResourceId": "run_prompt",
    "inputValues": {
      "payload": {
        "type": "JSON_LITERAL",
        "value": "{\"aiConfigId\":\"{AI_CONFIG_ID}\",\"capabilityId\":\"{CAPABILITY_ID}\",\"skillConfigId\":\"{SKILL_CONFIG_ID}\",\"inputAttributeMappings\":[{\"id\":\"{INPUT_ATTRIBUTE_ID}\",\"dataType\":\"string\",\"value\":\"{USER_INPUT}\"}],\"skillDatasetDetails\":{\"testDatasetId\":\"{TEST_DATASET_ID}\",\"testRecordId\":\"{TEST_RECORD_ID}\"},\"mode\":\"async\"}"
      }
    }
  }
]

 

What to replace (all of these come straight from the /exec request you captured in the Network tab, except where noted):

 

  • {YOUR_INSTANCE}: your instance name.
  • {DEFINITION_SYS_ID}: the Data Broker transform definition sys_id from the captured call.
  • {AI_CONFIG_ID}, {CAPABILITY_ID}, {SKILL_CONFIG_ID}: your skill’s IDs.
  • {INPUT_ATTRIBUTE_ID}: the input attribute mapping id for your skill input.
  • {USER_INPUT}: the value you want the skill to reason about (e.g. mixed-use-buildings). This is the one value you’ll change per call.
  • {TEST_DATASET_ID}, {TEST_RECORD_ID}: from the captured call.

 

A 200 here means the skill was started. From the response, grab the batch run ID and the result ID, since you need both for call 2.

 

Call 2: Retrieve the result via batch

 

The retrieval goes through the standard ServiceNow batch API, which wraps two Data Broker sub-requests: one to get the result by its ID, and one to get batch statuses by run ID.

 

POST https://{YOUR_INSTANCE}.service-now.com/api/now/v1/batch

Authorization: Basic (username: api_user, password: {YOUR_PASSWORD})
Content-Type: application/json

 

{
  "batch_request_id": "{ANY_UNIQUE_UUID}",
  "enforce_order": false,
  "rest_requests": [
    {
      "id": "{ANY_UNIQUE_UUID}",
      "method": "POST",
      "options": { "is_encoded": false, "should_encode_response": false },
      "url": "/api/now/uxf/databroker/exec",
      "headers": [
        { "name": "X-WantAuthSessionNotifications", "value": true },
        { "name": "X-UserToken", "value": "{X_USER_TOKEN}" }
      ],
      "body": [
        {
          "type": "TRANSFORM",
          "definitionSysId": "{GET_RESULT_DEFINITION_SYS_ID}",
          "parentResourceId": "get_batch_result_by_id",
          "inputValues": {
            "batch_result_id": {
              "type": "JSON_LITERAL",
              "value": "{RESULT_ID_FROM_CALL_1}"
            }
          }
        }
      ]
    },
    {
      "id": "{ANY_UNIQUE_UUID}",
      "method": "POST",
      "options": { "is_encoded": false, "should_encode_response": false },
      "url": "/api/now/uxf/databroker/exec",
      "headers": [
        { "name": "X-WantAuthSessionNotifications", "value": true },
        { "name": "X-UserToken", "value": "{X_USER_TOKEN}" }
      ],
      "body": [
        {
          "type": "TRANSFORM",
          "definitionSysId": "{GET_STATUS_DEFINITION_SYS_ID}",
          "parentResourceId": "get_batch_result_statuses_by_run_id",
          "inputValues": {
            "batch_run_id": {
              "type": "JSON_LITERAL",
              "value": "{BATCH_RUN_ID_FROM_CALL_1}"
            }
          }
        }
      ]
    }
  ]
}

 

What to replace:

 

  • {ANY_UNIQUE_UUID}: any UUIDs you generate to identify the batch and its items.
  • {X_USER_TOKEN}: a valid user token (captured from the Network tab, or generated for your session).
  • {GET_RESULT_DEFINITION_SYS_ID} and {GET_STATUS_DEFINITION_SYS_ID}: the transform definition sys_ids for get_batch_result_by_id and get_batch_result_statuses_by_run_id respectively (from the captured batch call).
  • {RESULT_ID_FROM_CALL_1}: the result ID returned by call 1 (this retrieves the actual answer).
  • {BATCH_RUN_ID_FROM_CALL_1}: the batch run ID returned by call 1 (this retrieves status).

 

One thing to watch here: the two sub-requests want different IDs. get_batch_result_by_id needs the result ID, while get_batch_result_statuses_by_run_id needs the batch run ID. Mixing these up returns 200 with no usable result, which is exactly what tripped me up on stream. Since call 1 can hold multiple requests, you get multiple result IDs, so make sure you use the result ID that maps to the request you actually care about.

 

Once the IDs line up and the skill has finished, the batch response contains your model output: the same recommendation the synchronous method returns, just retrieved through more steps.

 

↑ Return to Table of Contents

 

Debugging: the Generative AI Log table

 

When a call “works” (200) but you get nothing useful back, your best tool is the Generative AI Log table: sys_generative_ai_log. Every generative AI execution lands here with the prompt that was sent, the response, whether it succeeded, and any error code.

 

Use it to answer the two questions behind most failures:

 

  • Did the skill actually receive my input? If the logged prompt shows the input as undefined or null, your payload key doesn’t match the skill’s input name. In the Scripted REST API, that’s the left-hand side of inputsPayload['something_you_like'], and it must match your skill’s input exactly. This is the common “I sent user_input but the skill expects something_you_like” mismatch.
  • Did it run and produce a response I’m just not returning? If the log shows a successful response but your caller got an empty object, the skill is fine, and your retrieval or parsing is the problem (wrong ID in the Data Broker batch call, or not reading .result correctly).

 

A couple more tips:

 

  • Intermittent errors on every AI call often just mean the service was temporarily unavailable. Before you tear your integration apart, wait a bit and retry, and make sure your Now Assist plugins are current.
  • For the Data Broker route, the Network tab (F12) is the reliable reference. If your hand-built payload fails, compare it against the one the platform sends. The usual culprits are array wrapping, the stringified payload.value, and the result-vs-run-ID distinction.

 

↑ Return to Table of Contents

 

Watch the livestreams

 

This walkthrough came out of two Live Coding with Earl streams where I worked the whole thing out on air, building the skill, hitting walls, and landing both methods. If you want to see it happen end to end (debugging and all), here are both parts.

 

Part 1, building the skill and the first attempts:

 

 

Part 2, solving both the Scripted REST API and Data Broker methods:

 

 

↑ Return to Table of Contents

 

Wrap-up

 

You now have two working ways to invoke a ServiceNow AI Skill from anywhere that can send an HTTP request:

 

  • Scripted REST API: synchronous, one call, one script you fully control. This is the recommended default.
  • Data Broker: asynchronous, two calls, no custom server script, though it takes more payload wrangling. Useful when you want the asynchronous request-then-retrieve pattern the platform uses internally.

 

The benefit that makes this worth doing: the skill runs on the instance, so it keeps native access to your data, your table rows, and the security model of the user making the request. Since it’s now callable over REST, you can trigger all of that from an off-platform automation and reuse the same skill wherever you need it.

 

If you build something with this, I’d love to hear about it in the comments.

 

↑ Return to Table of Contents