Will Hallam
ServiceNow Employee

DISCLAIMER: The examples in this article come with no support or warranty, explicit or implied.  Caveat Emptor!

 

When it comes to getting your hands around the burgeoning swarm of AI resources in your IT estate, ServiceNow brings multiple tools into the fray.

 

At a platform level, the AI Control Tower provides the means to inventory external AI resources via its AI Discovery capability.  This works via various Service Graph Connectors which ingest AI resources from hyperscalers and AI providers, creating asset records on the platform which reflect what exists in those external systems.

 

Within ITOM, you now have additional discovery patterns available via the store app "AI Agent Topology Mapping".  These work with AWS Bedrock and Azure Foundry Agents today, with more coming in future releases.  These discovery patterns will create configuration items in your CMDB which align to those external AI resources, as well as related asset records.

 

Currently, if you use both capabilities on AWS Bedrock, there are some rough edges when it comes to combining the data from each source.  The two modules use slightly different naming for the assets, resulting in some duplicates.  Pending an official update which resolves these issues, here is one example of how they can be smoothed.

 

"Why?"

 

For those customers with ITOM, here's why using AI Agent Topology Mapping, alone or in concert with the AI Discovery included with AI Control Tower, is compelling:

- it populates the CMDB, whereas AICT alone will only create assets

- it brings in the tag metadata placed on AI components by the cloud providers, which then lets you link them to supporting infrastructure via tag-based service mapping

- it runs as part of existing cloud discovery schedules, no separate setup required

 

If one or more of those points strikes a chord, here's an example of how to align the pattern for AWS Bedrock Agents to dovetail seamlessly with AI Control Tower.

 

Pattern Step Modification

 

In the "Amazon AWS - Bedrock Agent" pattern, the following change aligns the CI and asset creation to the standard expected by AI Control Tower:

- In main section, "Identification of Amazon AWS - Bedrock Agents", Step 8 ("Get Agent"), replace the EVAL script with the following:

/**
 * Discovery Pattern Eval Script — Amazon Bedrock Agents (Version-per-CI)
 *
 * Change from OOB:
 *   OOB emits one row per agent (base agent detail only).
 *   This version calls ListAgentVersions for each agent_id and emits
 *   one row per version (including DRAFT), so Cloud Discovery creates
 *   one cmdb_ci_function_ai CI per version — matching the SGC's
 *   one-asset-per-version model and enabling ARN-based coalesce.
 *
 * Versioned ARN format (matches SGC external_ref_id):
 *   arn:aws:bedrock:{region}:{account}:agent/{agentId}/agentversions/{version}
 *   e.g. .../agentversions/1  or  .../agentversions/DRAFT
 *
 * Display name format (matches SGC display_name):
 *   "{agentName} (v{n})"  for numbered versions
 *   "{agentName} (DRAFT)" for the working draft
 */

var serviceAccount = CTX.getAttributes().get("service_account");
var credAlias     = CTX.getAttributes().get("credential_alias");
var resultList    = new Packages.java.util.ArrayList();

try {
    var region      = ${cmdb_ci_aws_datacenter[1].name};
    var headers     = "Content-Type: application/json, Accept: application/json";
    var formatted   = "true";

    // Base URL for GetAgent (GET) — same as OOB
    var getAgentUrlTemplate  = ${awsUrl} + "/{agent-id}";
    getAgentUrlTemplate = getAgentUrlTemplate.replace("{region}", region);

    // URL for ListAgentVersions (POST) — appends /agentversions/
    var listVersionsUrlTemplate = ${awsUrl} + "/{agent-id}/agentversions/";
    listVersionsUrlTemplate = listVersionsUrlTemplate.replace("{region}", region);

    var agent_ids        = ${response_parsed[*].agent_id}.toArray();
    var cloudRestQueryUtil = new CloudRestQueryUtil();
    var headersMap       = cloudRestQueryUtil.parseHeader(headers);

    for (var i = 0; i < agent_ids.length; i++) {
        var agent_id = agent_ids[i];

        // ── Step 1: Get base agent detail (same as OOB) ──────────────────────
        var getUrl = getAgentUrlTemplate.replace("{agent-id}", agent_id);
        var agentResult = cloudRestQueryUtil.cloudApiExecute(
            serviceAccount, credAlias, getUrl, headersMap, "GET", null, formatted, CTX
        );
        if (agentResult == null) continue;
        var agentObj = JSON.parse(agentResult);
        var agent    = agentObj.agent;

        // Base (unversioned) ARN from the agent detail response
        var baseArn  = agent.agentArn || "";

        // ── Step 2: List all versions for this agent ─────────────────────────
        var listUrl        = listVersionsUrlTemplate.replace("{agent-id}", agent_id);
        var listBody       = JSON.stringify({ maxResults: 100 });
        var versionsResult = cloudRestQueryUtil.cloudApiExecute(
            serviceAccount, credAlias, listUrl, headersMap, "POST", listBody, formatted, CTX
        );
        if (versionsResult == null) continue;
        var versionsObj      = JSON.parse(versionsResult);
        var versionSummaries = versionsObj.agentVersionSummaries || [];

        // ── Step 3: Emit one row per version ─────────────────────────────────
        for (var v = 0; v < versionSummaries.length; v++) {
            var vs      = versionSummaries[v];
            var version = vs.agentVersion + ""; // "DRAFT" or "1", "2", …

            // Versioned ARN — matches SGC external_ref_id format
            var versionedArn = baseArn + "/versions/" + version;

            // Display name — matches SGC format:
            // {glide.appcreator.company.friendly_name} {agentName} {version}
            // e.g. "Service-now.com aict-demo-code-review-agent DRAFT"
            // NOTE: glide.appcreator.company.friendly_name is not available in
            // Cloud Discovery eval context (runs outside SN scripting engine).
            // The display_name is therefore set to "{agentName} {version}" here
            // and the pre/post sensor overrides it using buildDisplayName() which
            // DOES have access to gs.getProperty(). If the transform writes
            // display_name from this field before the sensor runs, the sensor
            // will correct it on the asset record.
            var displayName = agent.agentName + ' ' + version;

            var row = new Packages.java.util.HashMap();

            row.put("arn",             versionedArn);          // versioned — coalesce key
            row.put("base_arn",        baseArn);               // unversioned — kept for reference
            row.put("agent_id",        agent_id);
            row.put("agent_version",   version);
            row.put("agent_name",      agent.agentName    || "");
            row.put("display_name",    displayName);
            row.put("agent_status",    vs.agentStatus     || agent.agentStatus || "");
            row.put("foundation_model",agent.foundationModel || "");
            row.put("instruction",     agent.instruction  || "");
            row.put("agent_description", agent.description || vs.description || "");

            resultList.add(row);
        }
    }

    CTX.setAttribute("response_parsed1", resultList);

} catch (error) {
    var msg = "Error in Bedrock agent version discovery: " + error;
    Packages.com.snc.sw.log.DiscoLog.getLogger(
        "Amazon AWS - Exception in version-per-CI Bedrock agent eval"
    ).error(msg);
}

This will generate a CI for each agent version, with an object_id in the format that AI Control Tower expects.

 

Disable Get Agent Version Extension

In the "Amazon AWS - Bedrock Agent" pattern, you need to disable the extension named "Get Agent Version", so that the agent version is not overwritten in each CI.  The default behavior of creating one agent CI conflicts with the AICT expectation that there will be a separate asset for each version of the Bedrock agent.  The AICT approach makes more sense, IMO, because you can have those multiple versions available for your apps to use, so they should be tracked and managed discreetly.

 

Pattern Extension Modification

 

- In pattern "Amazon AWS - Bedrock Agent", edit the "AWS Agent Bedrock Tags" extension, step 4 ("Reference between main_ci and cmdb_key_value") so that instead of "object_id"->"object_id", it joins on "object_id"->"base_arn".

WillHallam_0-1781632554780.png

 

Pre/post Script Modification

 

- Update script "Create Asset Classes and Product Model Classes for Bedrock" as follows:

/**
 * Pre/Post Sensor Script — Bedrock Agent Asset + Product Model Creation
 *
 * Changes from OOB:
 *
 * 1. COALESCE KEY — external_ref_id on alm_ai_system_digital_asset set to the
 *    VERSIONED ARN from object_id (e.g. .../agentversions/1, .../agentversions/DRAFT).
 *
 * 2. VERSION derived from object_id, NOT product_instance_id.
 *    product_instance_id is unreliable — OOB pattern transform overwrites it
 *    with the base agent version number for all CIs including DRAFT.
 *    object_id = versioned ARN; split('/versions/');
 *    e.g. ".../agentversions/DRAFT" → "DRAFT"
 *         ".../agentversions/1"     → "1"
 *
 * 3. PRODUCT MODEL NAME = bare {agentName} only (no company prefix, no version).
 *    Platform auto-display on alm_asset = "{manufacturer.name} {model.name} {version}"
 *    = "Service-now.com {agentName} {version}" — matches SGC output exactly.
 *    Adding the prefix to model.name causes it to appear twice.
 *
 * 4. PROVIDER / VENDOR alignment with SGC:
 *    - Product model manufacturer = COMPANY_NAME ("Service-now.com") → Provider
 *    - Asset vendor = AWS → Vendor
 */

var rtrn = {};

var is_model = true;
var asset_ci_rel_deployment_of = '76535b1cff0232103cf3ffffffffffc0';
var bedrockIds = [];

function extractBedrockIdsFromPayload(payloadObj) {
    for (var itemIndex in payloadObj.items) {
        var traversedObject = payloadObj.items[itemIndex];
        if (traversedObject.className == 'cmdb_ci_function_ai') {
            bedrockIds.push(traversedObject.sysId + '');
        }
    }
}

function getModelCategory(input) {
    var gr = new GlideRecord('cmdb_model_category');
    gr.addQuery('name', input);
    gr.query();
    if (gr.next()) return gr.getUniqueValue();
    return '';
}

function getManufacturer(input) {
    var gr = new GlideRecord('core_company');
    gr.addQuery('name', input);
    gr.query();
    if (gr.next()) {
        return gr.getUniqueValue();
    } else {
        var gr1 = new GlideRecord('core_company');
        gr1.initialize();
        gr1.setValue('name', input);
        return gr1.insert();
    }
}

function createCiAssetRelationship(ci_sys_id, asset_sys_id, rel_id) {
    var ciAssetRel = new GlideRecord('cmdb_rel_asset_ci');
    ciAssetRel.addQuery('asset', asset_sys_id);
    ciAssetRel.addQuery('configuration_item', ci_sys_id);
    ciAssetRel.addQuery('rel_type', rel_id);
    ciAssetRel.query();
    if (!ciAssetRel.next()) {
        var ciAssetReln = new GlideRecord('cmdb_rel_asset_ci');
        ciAssetReln.setValue('asset', asset_sys_id);
        ciAssetReln.setValue('configuration_item', ci_sys_id);
        ciAssetReln.setValue('rel_type', rel_id);
        ciAssetReln.insert();
    }
}

/**
 * Extracts version token from a versioned ARN.
 * ".../agentversions/DRAFT" → "DRAFT"
 * ".../agentversions/1"     → "1"
 * Returns "" if pattern not found.
 */
function versionFromArn(arn) {
    if (!arn) return '';
    var parts = (arn + '').split('/versions/');
    return parts.length > 1 ? parts[1] : '';
}

function createOrUpdateAssetAndProductModelClass() {
    var companyName    = gs.getProperty('glide.appcreator.company.friendly_name') || 'ServiceNow';
    var snManufacturer = getManufacturer(companyName);  // Provider = Service-now.com
    var awsVendor      = getManufacturer('Amazon');        // Vendor = AWS

    for (var i = 0; i < bedrockIds.length; i++) {
        var bedrock_id = bedrockIds[i];

        var grAi = new GlideRecord('cmdb_ci_function_ai');
        grAi.addQuery('sys_id', bedrock_id);
        grAi.query();
        if (!grAi.next()) continue;

        var agent_name        = grAi.getValue('name');
        var short_description = grAi.getValue('short_description');
        var object_id         = grAi.getValue('object_id');  // versioned ARN

        // Derive version from ARN — reliable regardless of product_instance_id
        var version = versionFromArn(object_id);

        var attributes = grAi.getValue('attributes') || '';
        var attr_array = attributes.split('||||');

        var instruction = '';
        var status      = '';
        if (attr_array.length > 0) {
            var p0 = attr_array[0].split('=');
            instruction = p0.length > 1 ? p0[1] : '';
        }
        if (attr_array.length > 1) {
            var p1 = attr_array[1].split('=');
            status = p1.length > 1 ? p1[1] : '';
        }

        var system_component_model_category = getModelCategory('Agentic AI');
        var prompt_model_category           = getModelCategory('AI prompt');

        // Product model name = bare agent name only.
        // Platform builds display_name as "{manufacturer.name} {model.name} {version}"
        // = "Service-now.com {agentName} {version}" — exact SGC match.
        var product_model_name = agent_name;

        // ── Prompt asset ──────────────────────────────────────────────────────
        var prompt_product_model_id = '';
        var prompt_asset_id         = '';

        var promptAsset = new GlideRecord('alm_ai_prompt_digital_asset');
        promptAsset.addQuery('ci', bedrock_id);
        promptAsset.query();
        if (promptAsset.next()) {
            prompt_product_model_id = promptAsset.getValue('model');
            prompt_asset_id         = promptAsset.getUniqueValue();
            promptAsset.setValue('model',          prompt_product_model_id);
            promptAsset.setValue('model_category', prompt_model_category);
            promptAsset.setValue('prompt_info',    instruction);
            promptAsset.setValue('ci',             bedrock_id);
            promptAsset.update();

            var promptProdMod1 = new GlideRecord('cmdb_ai_prompt_product_model');
            promptProdMod1.addQuery('sys_id', prompt_product_model_id);
            promptProdMod1.query();
            if (promptProdMod1.next()) {
                promptProdMod1.setValue('name',                product_model_name);
                promptProdMod1.setValue('manufacturer',        snManufacturer);
                promptProdMod1.setValue('cmdb_model_category', prompt_model_category);
                promptProdMod1.update();
            }
        } else {
            var promptProdMod2 = new GlideRecord('cmdb_ai_prompt_product_model');
            promptProdMod2.initialize();
            promptProdMod2.setValue('name',                product_model_name);
            promptProdMod2.setValue('manufacturer',        snManufacturer);
            promptProdMod2.setValue('cmdb_model_category', prompt_model_category);
            prompt_product_model_id = promptProdMod2.insert();

            var promptAsset1 = new GlideRecord('alm_ai_prompt_digital_asset');
            promptAsset1.setValue('model',          prompt_product_model_id);
            promptAsset1.setValue('model_category', prompt_model_category);
            promptAsset1.setValue('prompt_info',    instruction);
            promptAsset1.setValue('ci',             bedrock_id);
            prompt_asset_id = promptAsset1.insert();
        }

        // ── Foundation model asset ────────────────────────────────────────────
        var model_model_category = getModelCategory('AI model');
        var model_arn            = '';
        var model_number         = '';
        var model_name           = '';
        var model_description    = '';
        var model_manufacturer   = '';

        if (attr_array.length > 2) { var p2 = attr_array[2].split('='); model_arn         = p2.length > 1 ? p2[1] : ''; }
        if (model_arn == null || model_arn == '' || model_arn == 'null') {
            is_model = false;
        } else {
            is_model = true;
            if (attr_array.length > 3) { var p3 = attr_array[3].split('='); model_number      = p3.length > 1 ? p3[1] : ''; }
            if (attr_array.length > 5) { var p5 = attr_array[5].split('='); model_name        = p5.length > 1 ? p5[1] : ''; }
            if (attr_array.length > 6) { var p6 = attr_array[6].split('='); model_description = p6.length > 1 ? p6[1] : ''; }
            if (attr_array.length > 7) { var p7 = attr_array[7].split('='); model_manufacturer = getManufacturer(p7.length > 1 ? p7[1] : ''); }
        }

        var model_product_model = '';
        var model_asset_id      = '';

        if (is_model) {
            var modProd = new GlideRecord('cmdb_ai_model_product_model');
            modProd.addQuery('name',                model_name);
            modProd.addQuery('model_number',        model_number);
            modProd.addQuery('cmdb_model_category', model_model_category);
            modProd.addQuery('manufacturer',        model_manufacturer);
            modProd.query();
            if (modProd.next()) {
                model_product_model = modProd.getUniqueValue();
            } else {
                var aiModProd = new GlideRecord('cmdb_ai_model_product_model');
                aiModProd.setValue('name',                model_name);
                aiModProd.setValue('model_number',        model_number);
                aiModProd.setValue('cmdb_model_category', model_model_category);
                aiModProd.setValue('manufacturer',        model_manufacturer);
                model_product_model = aiModProd.insert();
            }

            var modDig = new GlideRecord('alm_ai_model_digital_asset');
            modDig.addQuery('model',           model_product_model);
            modDig.addQuery('model_category',  model_model_category);
            modDig.addQuery('vendor',          model_manufacturer);
            modDig.addQuery('external_ref_id', model_arn);
            modDig.query();
            if (modDig.next()) {
                model_asset_id = modDig.getUniqueValue();
                modDig.setValue('display_name', model_name);
                modDig.update();
            } else {
                var modDigital = new GlideRecord('alm_ai_model_digital_asset');
                modDigital.setValue('model',           model_product_model);
                modDigital.setValue('model_category',  model_model_category);
                modDigital.setValue('display_name',    model_name);
                modDigital.setValue('vendor',          model_manufacturer);
                modDigital.setValue('external_ref_id', model_arn);
                model_asset_id = modDigital.insert();
            }
        }

        // ── Agent System asset ────────────────────────────────────────────────
        var agent_asset_id         = '';
        var agent_product_model_id = '';

        var aiSystem = new GlideRecord('alm_ai_system_digital_asset');
        aiSystem.addQuery('external_ref_id', object_id);  // versioned ARN coalesce
        aiSystem.query();

        if (aiSystem.next()) {
            agent_asset_id         = aiSystem.getUniqueValue();
            agent_product_model_id = aiSystem.getValue('model');

            aiSystem.setValue('ci',             bedrock_id);
            aiSystem.setValue('vendor',         awsVendor);
            aiSystem.setValue('install_status', status == 'PREPARED' ? '31' : '33');
            aiSystem.setValue('ai_prompts',     prompt_asset_id);
            if (model_asset_id != '') aiSystem.setValue('ai_models', model_asset_id);
            aiSystem.update();

            var grSystemComp = new GlideRecord('cmdb_ai_system_component_product_model');
            grSystemComp.addQuery('sys_id', agent_product_model_id);
            grSystemComp.query();
            if (grSystemComp.next()) {
                grSystemComp.setValue('name',              product_model_name);
                grSystemComp.setValue('manufacturer',      snManufacturer);
                grSystemComp.setValue('version',           version);
                grSystemComp.setValue('status',            status);
                grSystemComp.setValue('short_description', short_description);
                grSystemComp.update();
            }

        } else {
            var grSysComp = new GlideRecord('cmdb_ai_system_component_product_model');
            grSysComp.initialize();
            grSysComp.setValue('name',              product_model_name);  // bare agentName
            grSysComp.setValue('manufacturer',      snManufacturer);      // Service-now.com
            grSysComp.setValue('version',           version);             // DRAFT or 1
            grSysComp.setValue('status',            status);
            grSysComp.setValue('short_description', short_description);
            grSysComp.setValue('cmdb_model_category', system_component_model_category);
            agent_product_model_id = grSysComp.insert();

            var aiSysDig = new GlideRecord('alm_ai_system_digital_asset');
            aiSysDig.setValue('model',           agent_product_model_id);
            aiSysDig.setValue('model_category',  system_component_model_category);
            aiSysDig.setValue('install_status',  status == 'PREPARED' ? '31' : '33');
            aiSysDig.setValue('external_ref_id', object_id);   // versioned ARN
            aiSysDig.setValue('vendor',          awsVendor);   // AWS
            aiSysDig.setValue('ai_prompts',      prompt_asset_id);
            aiSysDig.setValue('ai_models',       model_asset_id);
            aiSysDig.setValue('ci',              bedrock_id);
            agent_asset_id = aiSysDig.insert();
        }

        createCiAssetRelationship(grAi.getUniqueValue(), agent_asset_id, asset_ci_rel_deployment_of);
    }
}

if (payloadRecords) {
    while (payloadRecords.next()) {
        var payloadObj = JSON.parse(payloadRecords.ie_output + '');
        extractBedrockIdsFromPayload(payloadObj);
        createOrUpdateAssetAndProductModelClass();
        payloadObj = null;
    }
}

rtrn = {
    'status': {
        'message': 'Asset and Product Model Classes Created Successfully',
        'isSuccess': true
    },
    'patternId': patternId
};

This will align the company and manufacturer attributes to match what AI Control Tower is using.

 

Be sure to follow good update set practices and remember to revert and clean up these user update records (sys_update_xml) when the official fix comes out.

Version history
Last update:
5 hours ago
Updated by:
Contributors