Friday, June 20, 2025

Managing ServiceNow Storage Effectively: Tips, Pitfalls, and AI Opportunities

Introduction

As organizations grow, so does their ServiceNow data. From audit logs to attachments and historical records, an unmanaged instance can quickly exceed storage limits — leading to performance degradation, license breaches, or even functionality risks.

Storage management isn't a static problem either. Two things keep reshaping it: cloning activity that spikes logs and errors in ways that catch teams off guard, and newer platform capabilities — AI Search, Now Assist, AI Agents — that introduce entirely new categories of storage consumption nobody was tracking a couple of years ago. This article covers the fundamentals, corrects a couple of mechanics that are easy to get wrong, and then covers both of those newer wrinkles directly.

1. Understand What's Consuming Space

Start by identifying top storage consumers:

  • Navigate to Instance Usage > Application Usage Overview for the officially reported Primary DB size and top-level breakdown.
  • Focus on heavy tables like sys_audit, sys_email, sys_attachment, task, and cmdb_ci.

A word of caution before reaching for a script here: ServiceNow restricts direct SQL access to instances for security reasons, so there isn't a built-in scriptable API that hands back exact per-table byte sizes the way a database admin tool would. If you need an approximation from script rather than the UI, the approach below samples a limited number of records and estimates from there — it's directional, not exact, and gets slower and less representative the larger the table is.

function estimateTableSize(tableName, sampleLimit) {
    var gr = new GlideRecord(tableName);
    gr.setLimit(sampleLimit || 10000);
    gr.query();

    var totalBytes = 0;
    var count = 0;
    while (gr.next()) {
        for (var field in gr) {
            totalBytes += gr[field].toString().length;
        }
        count++;
    }
    gs.print(tableName + ': sampled ' + count + ' records, ~' + (totalBytes / 1048576).toFixed(2) + ' MB in sample');
}

estimateTableSize('incident', 10000);

For an authoritative figure on very large tables, ServiceNow Support can provide an official database footprint report through the Now Support portal — worth requesting directly rather than relying on script-based estimates when the exact number actually matters (for license or capacity conversations, for instance).

2. Use Table Cleaner (Auto Flush) for Log and System Tables

For fast-growing system tables, Table Cleaner — the underlying mechanism behind what's commonly called "Auto Flush" — is the recommended method to manage data safely and efficiently, without hand-rolled deletion scripts.

Examples of flushable tables:

  • syslog_transaction
  • syslog
  • sys_email
  • sys_audit_delete
  • ecc_queue

Configure via: Navigate to System Maintenance > Table Cleanup (these records are labeled "Auto Flushes"), or type sys_auto_flush_list.do directly into the Navigator filter.

Example: Auto-flush syslog_transaction older than 30 days

The actual fields on an Auto Flush record are Matchfield (the date/time field to check) and Age in seconds (a plain numeric value, not a script) — not a scripted condition or batch size field:

Table: syslog_transaction
Matchfield: sys_created_on
Age in seconds: 2592000

A few things worth knowing before relying on this:

  • The scheduled job that actually runs Table Cleaner (BulkTableCleaner) runs once per hour by default — don't expect near-real-time cleanup.
  • Table Cleaner skips tables subject to table rotation or table extension — for tables like sysevent and syslog on newer instances, rotation may already be handling this instead, so check which mechanism is actually active before assuming Auto Flush is doing the work.
  • Some default Auto Flush records exist for important reasons and shouldn't be casually deactivated. ServiceNow Support has specifically warned against deactivating the default record for workflow context cleanup, for example — doing so can let wf_context/wf_history grow unmanageably large. Understand what a default record is protecting against before turning it off.

✅ Auto-flush avoids scripting risks and runs in a controlled, platform-managed background process — a real advantage over ad hoc deletion scripts.

3. Use Retention Policies for Business Data

For structured business records (like incidents or change requests), use:

  • Auto Archive Rules for historical visibility.
  • Auto Delete Rules for permanent cleanup when archiving isn't required.
  • Data Retention Policies aligned with legal/compliance frameworks.

Avoid deleting directly via script unless absolutely necessary.

4. AI-Driven Optimization (Emerging Practice)

AI models can enhance storage strategies by:

  • Recommending purge targets based on usage frequency.
  • Highlighting duplicate or redundant attachments.
  • Surfacing patterns in job performance that would take much longer to spot manually.

Treat this as a direction to explore rather than a copy-paste script — the exact fields available for tracking job execution duration can vary by release and job type, so verify against your own instance's schema before building automation around a specific field name.

5. Post-Clone Storage Spikes: A Different Kind of Problem

This deserves its own section because it's a genuinely different failure mode than steady organic growth — it's a sudden spike triggered by an operational event, and it catches teams off guard specifically because it wasn't there yesterday.

Why clones spike storage and logs:

  • Reactivated integrations pointing at endpoints that no longer make sense in that environment. A clone brings over business rules, REST Message configurations, and scheduled jobs exactly as they existed in production — including ones that were deliberately disabled in the lower environment before the clone. If those integrations fire against production third-party endpoints from a non-prod instance (or simply fail because the target system, credentials, or network path isn't valid there), each failed attempt can generate its own error log entry, retry, and failure record. Run repeatedly on a schedule, this adds up fast.
  • Scheduled jobs resuming their original cadence. If job schedules were deliberately staggered or narrowed in scope in a lower environment before the clone (to avoid overloading a shared third-party source, for instance), a clone resets that back to the production schedule and scope by default — meaning full-volume pulls can resume running on a lower environment that was never sized or intended to handle that load, generating a correspondingly large volume of new records and log entries.
  • A burst of platform-level log activity from the clone process itself — plugin activation/reconciliation, table structure changes, and general clone housekeeping generate their own log volume in the days immediately following a clone, separate from anything integration-related.

What actually helps:

  • Don't rely on manually re-disabling or re-scoping integrations after every clone — that's the same recurring-toil problem covered in depth in this blog's integration best practices coverage on Preserver List & Cloning. The durable fix is the same one: gate environment-sensitive behavior behind a system property that's explicitly excluded from being overwritten by clone, so a lower environment stays safely configured through every future clone automatically, rather than depending on someone remembering to redo it.
  • Budget for a post-clone storage and log review as a standing checklist item, not an ad hoc reaction. Check sys_email, syslog, and ecc_queue specifically in the days following a clone — these are usually where a failing reactivated integration shows up first and most visibly.
  • If a lower environment consistently spikes after every clone for the same reason, that's a signal the underlying integration's environment-awareness needs fixing at the configuration level — not something to keep manually cleaning up after each time.

6. Newer Storage Consumers: AI and Beyond

Traditional "top tables" checklists were built around a platform that didn't have to account for what's now a real and growing category: AI-related data. As Now Assist, AI Search, and AI Agent capabilities have become more embedded in the platform, they've introduced storage consumption patterns that don't show up on an older list of "usual suspects":

  • AI Search indexing and embeddings — powering semantic search consumes storage in ways fundamentally different from traditional keyword-indexed data, and it scales with the content being indexed, not just with transactional record volume.
  • Now Assist interaction and session data — conversational AI interactions, summaries, and generated content can accumulate meaningfully over time, especially at higher adoption.
  • AI Agent execution history and traces — as agentic automation takes on more multi-step orchestration work, the execution trail it leaves behind is itself a new, growing data category.

Two practical takeaways:

  • Check whether your instance's Application Usage Overview breaks out AI-related consumption separately. ServiceNow has been building AI-specific usage visibility into its standard reporting as these features have matured — that's a more reliable starting point than trying to guess which underlying tables to watch, since the exact schema for these newer capabilities shifts release to release.
  • Don't assume your existing retention and Auto Flush strategy automatically covers these new categories. A cleanup strategy built entirely around sys_audit, sys_email, and sys_attachment won't necessarily catch a newer AI feature's storage growth unless someone deliberately extends the review to include it. Treat "are we covering the newer AI-related tables too" as a standing question during periodic storage reviews, not a one-time addition.

7. Common Pitfalls to Avoid

  • Overusing scripting to delete data: Prefer system-supported methods like Table Cleaner or retention rules.
  • Archiving ≠ deleting: Archives still consume space, though less than active records.
  • Uncoordinated full data pulls from PROD can lead to slowness, API throttling, or job failures.
  • Neglecting email and attachment tables, which silently grow large.

Script: Find attachments over 50MB, older than 1 year

var attach = new GlideRecord('sys_attachment');
attach.addEncodedQuery('size_bytes>52428800^sys_created_onRELATIVELE@year@ago@1');
attach.query();
while (attach.next()) {
  gs.print(attach.file_name + " — " + (attach.size_bytes / 1048576).toFixed(2) + " MB");
}

8. Enforce Limits for Integrations and APIs

  • Confirm your instance's REST query record-limit properties in System Properties (property names can vary somewhat by release — check your specific instance's REST-related properties rather than assuming a fixed name applies across all versions) to cap how much a single API response can return.
  • Restrict external integrations from triggering large, unbounded table queries.
  • Rate-limit ETL tools, or coordinate with those teams directly on full versus delta pull practices.

9. Collaborate with Data Consumers

  • Communicate with data warehouse/ETL teams using the Table API.
  • Insist that full-load testing happens in non-prod, never directly against production.
  • Prevent ad hoc, unattended test queries from running against live instances.

System Tables to Auto-Flush or Archive

Table Name Reason for Growth Recommended Action
sys_audit Field change logs Auto-archive or delete per retention policy
sys_email All email activity Auto-flush after retention window
syslog_transaction Transaction logs Auto-flush older entries
sys_audit_delete Deletion audit records Auto-flush per retention policy
sys_attachment_doc File storage (binary content) Identify and purge large/old attachments
ecc_queue Integration/MID Server traffic, including reactivated post-clone jobs Auto-flush, and review closely after every clone

Conclusion

Managing storage in ServiceNow is about more than just saving disk space. It's a proactive approach to maintaining performance, cost efficiency, and platform health. With the right mix of Table Cleaner (Auto Flush), retention rules, and AI-enhanced analysis, you can keep your instance lean and compliant — while avoiding manual, error-prone deletion methods.

Two things are worth carrying forward beyond the basics: storage problems aren't only steady organic growth — clone events create their own sharp, predictable spikes that deserve a standing checklist item, not a surprised reaction. And the definition of "what's consuming space" keeps expanding as the platform does — AI-related data is a real, growing category that an older storage strategy won't automatically account for. Building both into a periodic review, rather than treating storage management as a one-time setup, is what keeps this from becoming a recurring fire drill.

Enterprise Tips – Secure Credentials, Proxy Settings, and MID Server Options for PowerShell + ServiceNow API

Introduction

So far in this series, we've explored how to connect PowerShell to the ServiceNow Table API, handle errors, and optimize performance. But in enterprise environments, you'll run into real-world constraints like:

  • Secure credential storage
  • Network proxies
  • Internal ServiceNow instances behind firewalls
  • Compliance restrictions

In this final article, we cover how to run secure, robust API integrations in production environments using best practices and ServiceNow architecture features.

1. Securely Store and Use Credentials

Hardcoding usernames and passwords in scripts is a security risk. Before picking a storage method, it's worth connecting this back to Part 1 of this series: if you're authenticating with OAuth's Client Credentials grant (the current recommended approach), what you actually need to protect is a Client ID and Client Secret, not an end user's password. That's a meaningfully smaller attack surface than the credential-storage problem this section might otherwise imply — but it still needs protecting properly rather than living in plaintext in a script.

SecretManagement (Recommended Starting Point)

Microsoft's Microsoft.PowerShell.SecretManagement module is the current, cross-platform-friendly way to abstract secret storage — it works the same way whether the underlying vault is a local encrypted store, Azure Key Vault, HashiCorp Vault, or something else, so your script logic doesn't change if the backend does.

Install-Module Microsoft.PowerShell.SecretManagement -Scope CurrentUser
Install-Module Microsoft.PowerShell.SecretStore -Scope CurrentUser

# One-time: register a local vault and store the secret
Register-SecretVault -Name LocalVault -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
Set-Secret -Name "SNOW_CLIENT_SECRET" -Vault LocalVault

# In your script, retrieve it at runtime
$clientSecret = Get-Secret -Name "SNOW_CLIENT_SECRET" -Vault LocalVault -AsPlainText

Worth knowing: Microsoft has marked SecretManagement as feature-complete — it's still fully supported for security fixes, but not gaining new capabilities, specifically because the PowerShell team sees the industry moving toward passwordless and federated authentication (Entra ID, managed identities, hardware keys) rather than stored username/password secrets generally. That's not a reason to avoid it today — it's stable, well-documented, and does the job — but it's worth knowing this is a maturity/stability story, not an actively-evolving one, when planning long-term.

Windows Credential Manager (Simpler, Windows-Only)

For local automation on a Windows-only environment, the CredentialManager module wraps Windows Credential Manager directly:

Install-Module CredentialManager -Scope CurrentUser

# One-time setup
New-StoredCredential -Target "SNOW_API_CRED" -UserName "admin" -Password "your_password" -Persist LocalMachine

# In your script
$creds = Get-StoredCredential -Target "SNOW_API_CRED"
$user = $creds.Username
$pass = $creds.Password

This is simpler to set up than SecretManagement, but it doesn't support rotation, expiration, or audit logging the way a proper vault does, and it's Windows-specific — it won't work if any of your automation needs to run cross-platform under PowerShell 7+.

Secure Vaults (Enterprise)

If you're in a DevOps setup with centralized secret governance requirements, integrate with a dedicated vault directly — Azure Key Vault, HashiCorp Vault, or AWS Secrets Manager, each of which also has a SecretManagement extension vault available, letting you use the same Get-Secret/Set-Secret pattern above regardless of which one your organization standardizes on.

2. Use Proxy Settings When Required

Corporate environments often require internet access via proxy. PowerShell supports this:

$proxy = New-Object System.Net.WebProxy("http://proxy.company.com:8080")
$handler = New-Object System.Net.Http.HttpClientHandler
$handler.Proxy = $proxy

$client = [System.Net.Http.HttpClient]::new($handler)
$response = $client.GetAsync($url).Result

Or for Invoke-RestMethod (basic use):

Invoke-RestMethod -Uri $url -Proxy "http://proxy.company.com:8080" -Headers $headers

If the proxy requires authentication — common in corporate environments — pass credentials explicitly rather than assuming the default session context will satisfy it:

$proxyCred = Get-Secret -Name "PROXY_CRED" -Vault LocalVault
Invoke-RestMethod -Uri $url -Proxy "http://proxy.company.com:8080" -ProxyCredential $proxyCred -Headers $headers

3. Using MID Server as an Alternative to Direct API Calls

If ServiceNow is hosted internally or API access is restricted externally, a MID Server is the best approach.

What's a MID Server?

A Management, Instrumentation, and Discovery (MID) Server is a lightweight Java process that sits inside your network and acts as a secure bridge between ServiceNow and internal systems.

Use Cases:

  • When the target system is on-premises and ServiceNow's cloud instance can't reach it directly.
  • When you don't want to expose public API endpoints on your internal PowerShell-driven systems.
  • When calls need to run behind a proxy or firewall that only your internal network can traverse.

It's worth being precise about direction here, since these two mechanisms solve different problems:

  • A Scripted REST API is an inbound endpoint — it lets external systems (including a PowerShell script) call into ServiceNow. It processes whatever it receives using standard server-side script; it doesn't inherently involve a MID Server unless that processing logic separately needs to reach out to another on-prem system as part of handling the request.
  • A MID Server is what lets ServiceNow reach out to internal systems it can't call directly. If PowerShell needs to trigger something on an internal system via ServiceNow, or ServiceNow needs to pull data from an internal system, that's the MID Server's job — not the Scripted REST API's.

MID Server & Orchestration

You can use Orchestration + MID Server to trigger PowerShell scripts (via a PowerShell Probe) from a Workflow, and pull results back into ServiceNow. This is a well-established pattern, but it's tied to the classic Workflow engine. On current instances, the equivalent — and increasingly the more supported path going forward — is a Flow Designer flow calling the underlying probe API directly, or a vendor-provided Spoke if one exists for what you're trying to reach. Both approaches route through the same MID Server; the difference is which orchestration layer triggers it.

Bonus Tips

  • Understand rate limiting before assuming a number. ServiceNow doesn't enforce a single universal default rate limit like "100 calls/minute" — Rate Limit Rules are configured per-instance, typically per-hour rather than per-minute, and often aren't enabled at all unless a platform admin has explicitly set them up for specific users, roles, or APIs. Confirm what's actually configured on your instance with your platform team rather than assuming a fixed ceiling — building around an imagined limit can lead to either unnecessary self-throttling or an unpleasant surprise when a real one turns out to be lower than expected.
  • Rotate OAuth Client Secrets periodically, the same way you'd rotate a password.
  • Use roles and ACLs to limit what any given integration account can actually reach in ServiceNow — least privilege applies to service accounts as much as human users.
  • Log sensitive API interactions securely — don't let request/response logging become its own accidental data exposure.

Conclusion

Running PowerShell integrations with ServiceNow at scale requires more than just syntax — it takes planning for security, scalability, and reliability. By using proper secret storage (increasingly a Client Secret rather than a user password, if you followed this series' authentication recommendation), handling proxies correctly, and understanding what a MID Server actually does versus a Scripted REST API, you ensure your automation is enterprise-ready and compliant.

Optimizing Performance – Pagination, Filtering, and Query Design in PowerShell + ServiceNow API

Introduction

Once you've connected PowerShell to the ServiceNow Table API, the next big challenge is performance. Without the right approach, even a simple query can lead to:

  • Timeouts
  • Empty responses
  • Crashed scripts
  • Overloaded servers

This article covers three techniques to optimize your integration — pagination, field filtering, and efficient sysparm_query usage — plus a fix to a URL-encoding approach that trips up a lot of PowerShell + REST tutorials, including earlier drafts of this one.

1. Use Pagination (sysparm_limit and sysparm_offset)

By default, ServiceNow doesn't return all records in one call — and if you try to force it by omitting limits entirely, your script will time out on any table of meaningful size.

Best Practice

$limit = 100
$offset = 0
$headers = @{ "Authorization" = "Bearer $accessToken" }
$instance = "dev12345"
$url = "https://$instance.service-now.com/api/now/table/incident"

do {
    $pagedUrl = "$url?sysparm_limit=$limit&sysparm_offset=$offset"
    $response = Invoke-RestMethod -Uri $pagedUrl -Headers $headers
    $results = $response.result

    foreach ($record in $results) {
        Write-Output $record.number
    }

    $offset += $limit
} while ($results.Count -gt 0)

This loop pulls 100 records at a time — scalable, safe, and efficient. For genuinely large tables pulled on a schedule, consider adding a brief pause between pages (Start-Sleep -Milliseconds 200 or similar) — it costs very little wall-clock time overall and avoids hammering the instance with back-to-back requests, which matters more the more of these scripts you have running concurrently across environments.

2. Use sysparm_fields to Limit Response Size

By default, every API call returns all fields — even large ones like work_notes or attachments that you may not need for a given script.

Fix:

$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_fields=number,short_description,state"

This dramatically reduces payload size and speeds up execution — the effect compounds with pagination, since a smaller per-record payload means each page transfers and parses faster too.

3. Optimize Your sysparm_query Filter

Filtering is where most performance issues happen — especially when:

  • You use dot-walked fields.
  • You use LIKE queries.
  • You don't filter by time or indexed fields.

Bad:

caller_id.nameLIKEjohn

Causes implicit joins and slowdowns.

Good:

caller_id=681ccaf9c0a8016401c5a33be04be441

Add Time-Based Filters

Always use sys_updated_on or closed_at to narrow large tables:

sys_updated_on>javascript:gs.daysAgoStart(30)

A Correction Worth Making: How You URL-Encode the Query Matters

If you're building a sysparm_query value dynamically and need to URL-encode it before appending it to the request, it's common to see this pattern in PowerShell + REST tutorials:

$encodedQuery = [System.Web.HttpUtility]::UrlEncode($query)

This looks reasonable, but it has a real portability problem: System.Web.HttpUtility requires the System.Web assembly to be explicitly loaded first with Add-Type -AssemblyName System.Web — without that line, this throws an "unable to find type" error. It's also a largely Windows/.NET Framework-era namespace, which makes it an unreliable choice if any of your scripts might run under PowerShell 7+ (pwsh), including cross-platform or in a Linux-based CI/CD pipeline.

The more portable fix, available natively in both Windows PowerShell and PowerShell 7+ with no assembly loading required:

$encodedQuery = [System.Uri]::EscapeDataString($query)

This is also more RFC-compliant for query string encoding — HttpUtility.UrlEncode encodes spaces as + (an older HTML form-encoding convention), while EscapeDataString uses standard percent-encoding throughout. For a sysparm_query value being appended to a URL, EscapeDataString is the safer default.

Bonus: Combine All Techniques

$limit = 100
$offset = 0
$query = "active=true^sys_updated_on>javascript:gs.daysAgoStart(30)"
$encodedQuery = [System.Uri]::EscapeDataString($query)

do {
    $url = "https://$instance.service-now.com/api/now/table/incident?sysparm_query=$encodedQuery&sysparm_limit=$limit&sysparm_offset=$offset&sysparm_fields=number,short_description,state"

    $response = Invoke-RestMethod -Uri $url -Headers $headers
    $results = $response.result

    foreach ($incident in $results) {
        Write-Host "$($incident.number): $($incident.short_description)"
    }

    $offset += $limit
} while ($results.Count -gt 0)

Summary Checklist

Optimization Benefit
sysparm_limit + sysparm_offset Prevents timeouts, enables large pulls
sysparm_fields Reduces payload, faster API
Use sys_id instead of names Avoids joins
Filter on sys_updated_on Narrows down queries
Avoid dot-walked or LIKE filters Prevents performance bottlenecks
[System.Uri]::EscapeDataString() for encoding Portable across PowerShell versions, no assembly loading needed

Conclusion

A well-optimized query can save hours in execution time and avoid failed automations. These techniques are essential for scaling your PowerShell + ServiceNow integration reliably — and getting the small details right, like which URL-encoding method you reach for, is what separates a script that works on your machine from one that works reliably wherever it actually needs to run.

In the next article, we'll tackle real-world security and enterprise deployment tips, including proxies, secrets, and MID Server considerations.

Handling Common API Errors and Timeouts When Connecting to ServiceNow

ServiceNow common API errors and timeouts

Integrating PowerShell with the ServiceNow Table API mostly works — right up until a large table like incident, task, or cmdb_ci throws something unexpected: a script that crashes with no clear reason, a response that's missing fields you expected, or a call that just times out. This article covers the real causes behind the most common failures, and — just as importantly — the actual HTTP behavior involved, since getting that wrong leads to error-handling code that silently doesn't work.

Problem 1: "Transaction Cancelled – Maximum Execution Time Exceeded"

This happens when the server-side query takes too long to process — commonly on large tables, broad queries, or when heavy business logic runs on every matched record. The actual response comes back as HTTP 500 Internal Server Error, with this JSON body:

{
  "error": {
    "message": "Transaction cancelled: maximum execution time exceeded",
    "detail": "Transaction cancelled: maximum execution time exceeded Check logs for error trace or enable glide.rest.debug property to verify REST request processing"
  },
  "status": "failure"
}

This comes back as a real HTTP 500 error, not a 200. That distinction matters for how you handle it in PowerShell, covered in Problem 3 below. The default timeout is controlled by the glide.rest.max_execution_time_in_seconds property (60 seconds out of the box).

Why This Happens

  • You're pulling too many records at once.
  • Filters use unindexed or dot-walked fields (see Problem 2).
  • Long-running Business Rules or Flows are slowing down the underlying query.
  • You forgot to paginate or narrow the date range.

Fix It With PowerShell

# Correct use of pagination and fields
$limit = 100
$offset = 0
$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_limit=$limit&sysparm_offset=$offset&sysparm_fields=number,short_description,state"

$response = Invoke-RestMethod -Uri $url -Headers $headers
$response.result

Tip: always use sysparm_limit, sysparm_offset, and sysparm_fields together to reduce payload size and processing time per request — smaller, paginated calls are far less likely to hit the execution time ceiling than one large unbounded query.

Problem 2: Dot-Walked Field Filters Kill Performance

A filter like this:

caller_id.department.name=Finance

...is slow and prone to timing out, because dot-walking through reference fields in a query introduces implicit joins under the hood. The deeper the dot-walk, the more expensive the query gets.

Fix

Use direct sys_id values instead of dot-walking through display names:

$filter = "caller_id=6816f79cc0a8016401c5a33be04be441"
$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_query=$filter"

If you don't already know the sys_id you need, resolve it with a separate, targeted lookup first, rather than filtering the large table by a dot-walked value on every call.

Problem 3: Handling Real Errors in PowerShell Correctly

Since a timeout or server error comes back as a real HTTP 500 (or 400, 403, 404, depending on the failure), Invoke-RestMethod treats it as a terminating error by default in Windows PowerShell. That means an unhandled call doesn't quietly continue with an empty result — it stops your script entirely at that line, which is its own problem if you're not expecting it.

Add Proper Error Handling

try {
    $response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers -ErrorAction Stop
    Write-Host "✅ Records returned: $($response.result.Count)"
}
catch {
    $statusCode = $_.Exception.Response.StatusCode.value__
    $errorBody = $_.ErrorDetails.Message
    Write-Host "❌ API call failed with status $statusCode"
    Write-Host "Details: $errorBody"
}

On PowerShell 7+, Invoke-RestMethod also supports -SkipHttpErrorCheck, which lets you inspect $response.StatusCode directly without throwing — useful if you want to branch on specific status codes without a try/catch block:

$response = Invoke-RestMethod -Uri $url -Headers $headers -SkipHttpErrorCheck -StatusCodeVariable statusCode
if ($statusCode -ne 200) {
    Write-Host "❌ API Error ($statusCode): $($response.error.message)"
} else {
    Write-Host "✅ Records returned: $($response.result.Count)"
}

Problem 4: A 200 That Still Isn't What You Expected

There is a real scenario where a genuinely successful 200 OK response doesn't give you what you expected: Access Control Rules silently filtering data out of the response. If the account making the API call doesn't have read access to certain fields or records, ServiceNow doesn't return an error for that — it simply omits or blanks out what the caller isn't allowed to see. The call succeeds, the status is 200, and the response can still look "off" — fewer records than expected, or fields present in the schema but empty in every row.

If a script consistently returns fewer records or thinner data than expected with no error at all, checking the calling account's role and ACL access on the target table is usually a faster diagnosis than assuming it's a scripting bug.

Extra Debugging Tools

  • Enable REST logs in ServiceNow. Set glide.rest.debug = true temporarily (turn it back off afterward — this is verbose and not meant to run permanently).
  • Check syslog for REST errors. Navigate to System Logs > Errors.
  • Use Postman to isolate the failure. If a call fails from PowerShell but succeeds with identical parameters in Postman, the problem is in your PowerShell handling — headers, encoding, or error handling — not the API call itself.

Conclusion

Performance and error handling both matter here, but get the mechanics right first: ServiceNow's REST Table API returns real, standard HTTP status codes for real errors — it doesn't hide a 500-level failure behind a 200. The practical risk in PowerShell isn't a disguised error slipping past your checks; it's an unhandled terminating exception stopping your script cold, or an ACL quietly limiting what a 200 actually contains. Handle both, and paginate your queries with sysparm_limit/sysparm_offset/sysparm_fields, and most of what shows up here goes away.

Getting Started – PowerShell + ServiceNow Table API with Authentication

Introduction

ServiceNow's Table API provides full CRUD access to any record in the platform. Combine that with PowerShell, and you unlock the ability to automate ticketing, compliance tracking, CMDB updates, and more — right from the command line.

In this article, you'll learn how to:

  • Authenticate using Basic Auth, OAuth2 password grant (and why to avoid it), and OAuth2 Client Credentials grant (the current recommended approach)
  • Make your first Table API call from PowerShell
  • Parse and handle JSON responses
  • Set the stage for advanced integration in later posts

1. Basic Authentication Setup

This is the simplest approach, but it's not recommended for production. Basic Auth sends credentials with every single request, which is why it's generally treated as a legacy pattern for anything beyond quick local testing.

# Replace with your instance and credentials
$instance = "dev12345"
$user = "admin"
$pass = "your_password"
$base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$user`:$pass"))

# Define headers and URL
$headers = @{
    "Authorization" = "Basic $base64Auth"
    "Accept"        = "application/json"
}
$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_limit=1"

# Call the API
$response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers

# Output the result
$response.result

⚠️ Tip: Avoid hardcoding passwords, even for quick testing. Basic Auth is fine for a five-minute experiment against a Personal Developer Instance — don't build anything real on top of it.

2. OAuth2 Password Grant — and Why to Avoid It for New Work

This is the grant type shown in most older ServiceNow + PowerShell tutorials, including earlier drafts of this one. It's worth understanding, but it's no longer the right choice for anything new:

Prerequisites:

  • OAuth enabled in ServiceNow
  • A registered app with Client ID and Client Secret
  • A user account with API access
# Credentials and endpoint
$clientId = "your_client_id"
$clientSecret = "your_client_secret"
$username = "admin"
$password = "your_password"
$instance = "dev12345"
$tokenUrl = "https://$instance.service-now.com/oauth_token.do"

# Build request body
$body = @{
    grant_type = "password"
    client_id = $clientId
    client_secret = $clientSecret
    username = $username
    password = $password
}

# Get token
$response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body -ContentType "application/x-www-form-urlencoded"
$accessToken = $response.access_token

# Make Table API request
$headers = @{
    "Authorization" = "Bearer $accessToken"
    "Accept"        = "application/json"
}
$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_limit=1"
$data = Invoke-RestMethod -Uri $url -Method Get -Headers $headers

$data.result

Why this isn't actually the secure upgrade it looks like: this is the Resource Owner Password Credentials ("password") grant — and it's explicitly deprecated per current OAuth 2.0 security guidance, not just an older option among equals. Notice that it still requires the literal user password in the request body, which means switching from Basic Auth to this grant type doesn't actually solve the "avoid hardcoding passwords" problem — it just moves the same password from a request header into a request body. If you're on an older instance that doesn't yet support the grant type below, this remains a working fallback. For anything new, use Client Credentials instead.

3. OAuth2 Client Credentials Grant (Recommended)

Available since the Washington DC release, this grant is purpose-built for exactly this scenario — a script or service authenticating as itself, with no specific human user's password involved at all.

Prerequisites:

  • OAuth enabled in ServiceNow
  • A registered application configured for the Client Credentials grant, with an associated OAuth Application User (a dedicated service account used as the identity context for the token — configure this with the "Web service access only" / "Internal Integration User" option so it can't log into the UI and isn't subject to normal password expiration policies)
  • On Zurich and later, register this through System OAuth > Application Registry > New Inbound Integration Experience > New Integration > OAuth – Client Credentials grant. On older releases, use Create an OAuth API endpoint for external clients instead — the underlying mechanism is the same, just a different registration screen.
# Credentials and endpoint — no username or password needed
$clientId = "your_client_id"
$clientSecret = "your_client_secret"
$instance = "dev12345"
$tokenUrl = "https://$instance.service-now.com/oauth_token.do"

# Build request body
$body = @{
    grant_type    = "client_credentials"
    client_id     = $clientId
    client_secret = $clientSecret
}

# Get token
$response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body -ContentType "application/x-www-form-urlencoded"
$accessToken = $response.access_token

# Make Table API request
$headers = @{
    "Authorization" = "Bearer $accessToken"
    "Accept"        = "application/json"
}
$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_limit=1"
$data = Invoke-RestMethod -Uri $url -Method Get -Headers $headers

$data.result

Notice what's missing compared to the password grant: no username, no password. The script authenticates as the registered application itself, using the Client ID and Client Secret only. The Client Secret is still a credential worth protecting properly — that part of the "avoid hardcoding secrets" advice still applies, and we'll cover secure secret handling in Part 4 — but you're no longer storing an actual human user's password anywhere in the integration at all, which meaningfully reduces what's at risk if the script or its configuration ever leaks.

4. What You Should See

A single incident record, structured in JSON, regardless of which authentication method above you used:

{
  "result": [
    {
      "number": "INC0010001",
      "short_description": "Sample Incident",
      "state": "1",
      "sys_id": "abc123..."
    }
  ]
}

Conclusion

Connecting PowerShell to ServiceNow via the Table API is a powerful step toward automation. Whether you're managing incidents, risks, or CMDB items, understanding authentication methods — and which ones are actually current — is key. Basic Auth is fine for a quick local test; the password grant still works but is on its way out; Client Credentials is the one worth building on for anything you intend to keep running.

In future articles, we'll make this integration enterprise-grade with error handling, secure credential storage, and better performance.

Thursday, June 19, 2025

9 Ways to Break (and Fix) Field Auditing in ServiceNow

Introduction

Field auditing in ServiceNow is essential for compliance, troubleshooting, and change tracking — especially in regulated industries or IRM modules. But here's the kicker: it's surprisingly easy to break audit logging without even realizing it, and there are more ways to do it than most guides on this topic cover.

This article walks through nine ways audit logging can fail — some through misused code, some through platform behavior working exactly as designed but surprising people who don't know about it — and exactly how to fix or prevent each one.

1. Using setWorkflow(false) or autoSysFields(false)

What breaks: setWorkflow(false) disables the business rules that would normally fire for that transaction — and that includes the platform logic responsible for writing to sys_audit. This is the clearly-established cause of a silently unaudited change.

autoSysFields(false) is usually reached for alongside it, and definitely stops sys_updated_on, sys_updated_by, and sys_mod_count from updating. Its precise relationship to audit recording itself is murkier — even experienced practitioners on ServiceNow's own community forums note they're not entirely certain whether it independently affects what gets written to sys_audit, separate from setWorkflow(false)'s effect. What's clear either way: used together, as they usually are, the change becomes close to invisible through every normal means — no audit entry, no updated timestamp, no bumped modification count.

gr.setWorkflow(false);
gr.autoSysFields(false);
gr.update();  // No audit logged!

Fix: Only use these methods when you intentionally want to suppress system impact, such as during fix scripts or data loads. Avoid them in production logic that should be tracked, and if you must use them on a compliance-relevant table, pair the call with explicit manual logging so the change isn't invisible everywhere.

2. Updating Fields With No Real Change

What breaks: If you set a field to the same value it already had, ServiceNow does not log an audit entry — even though setValue() was called. This one isn't a suppression bug at all; it's the platform correctly recognizing that nothing actually changed.

current.setValue('state', current.state);  // No actual change → no audit

Fix: Only set a field when the value truly differs.

if (current.state != 'Closed') {
    current.setValue('state', 'Closed');
}

3. Subflows or Script Includes Rewriting Values Later

What breaks: A flow or script may initially update a field, which gets audited correctly — but later logic silently overwrites the field again, often via setWorkflow(false) or from a different execution path entirely, with no second audit entry to show it happened.

Fix:

  • Trace flows and subflows for any post-processing that touches audited fields.
  • Add temporary business rules to log who's modifying what, and when, while you're actively debugging.

4. Bulk Updates via Import Sets or Data Sources

What breaks: Import Sets and Transform Maps often run with Run Business Rules unchecked, which skips audit logging entirely for every record the load touches — not just one.

Fix:

  • Enable Run Business Rules on the Transform Map where audit history actually matters.
  • If you're doing custom ETL outside a standard Transform Map, build logging into the load job itself.
  • For compliance-relevant tables (IRM tables among them), avoid audit-suppressed bulk updates wherever it's genuinely avoidable.

5. Auditing Not Enabled on the Field

What breaks: The most basic — but still common — reason: the field was never marked for audit in the first place.

Fix:

  • Go to System Definition > Dictionary.
  • Search for the field and confirm Audit = true.
  • Re-save if needed. You can enable auditing retroactively for a field, but be clear about what that actually recovers: it starts tracking changes from that point forward. It doesn't reconstruct history for changes that happened before auditing was turned on — there's no data to recover if it was never written.

6. A Field Excluded via the no_audit Attribute — Even Though the Table Is Audited

What breaks: This one is sneakier than #5, because the symptoms look identical from the outside. It's not possible to audit individual fields without auditing the entire table — but it is possible to hide specific fields from an otherwise-audited table using the no_audit dictionary attribute. A developer checking "is this table audited?" and finding yes can still miss that one specific field was deliberately excluded.

Fix: If a specific field isn't showing history despite the table clearly being audited elsewhere, check that field's own dictionary entry for a no_audit attribute before assuming something else is broken.

7. Whitelist (Inclusion-List) Auditing Silently Limiting Scope

What breaks: By default, enabling auditing on a table audits every field except system fields — an exclusion-list approach. But a table can instead be configured with audit_type=whitelist, which flips the model entirely: only fields explicitly marked as audited get tracked, and everything else is excluded by design, not by accident. If you're troubleshooting a table configured this way without knowing it, "why isn't this field audited" can send you looking for a bug that isn't there.

Fix: Check the table's dictionary attributes for audit_type=whitelist before assuming a missing audit entry is a defect. If it's whitelist-configured, the fix is adding the specific field to the audited list, not debugging business rules.

8. Built-In Platform Exclusions You Didn't Know About

What breaks: A few exclusions are built into the platform by design, and they catch people who haven't run into them before:

  • Changes to fields with the sys_ prefix are excluded from auditing, with the exception of sys_class_name and sys_domain_id.
  • Updates made by the inactivity monitor are excluded, specifically to avoid a single stale record generating a flood of noise entries.

Fix: Nothing to fix here technically — but knowing these exclusions exist saves real debugging time when a field that "should" be audited turns out to be a system field, or when a record's staleness-triggered update never shows up in history.

9. Async-Triggered Updates Crediting "System" Instead of the Real User

What breaks: This is a different flavor of problem entirely — the audit entry exists, but who it credits is misleading. When a record is updated via an asynchronous mechanism (a Script Action responding to an event, for instance), the update typically runs as the system account by default. Audit History then shows User Name "System" and an empty User field, rather than the person whose action actually triggered the underlying event — even though a real user's action was what set the whole chain in motion.

Fix: If accurate attribution matters for a specific async-triggered flow, the triggering event's originating user (often available via the event or the originating record's sys_created_by/sys_updated_by) can be captured and applied manually before the update, with autoSysFields(false) and manually-set sys_created_by/sys_updated_by values, then re-enabled afterward. This is more setup than the default behavior, so reserve it for cases where attribution genuinely matters — for most async processing, "System" being credited is expected and fine.

Bonus: How to Audit-Proof Your Work

  • Use gs.info() logs during development to trace field changes as you build, not just when something's already gone wrong.
  • Add temporary After Update business rules to catch unexpected changes while investigating a specific issue.
  • Periodically review the sys_audit table directly to confirm field-level tracking is behaving the way you expect, rather than assuming the Dictionary configuration alone guarantees it.
  • Consider building a custom audit summary dashboard for sensitive tables — compliance/IRM tables like sn_compliance_policy_exception or sn_risk_risk are good candidates, given how much an auditor might eventually care about exactly this data.
  • If audit history for older records seems to be missing even though the table has always been audited, check whether a data retention or table cleanup policy is configured to purge old sys_audit records after a set period — "missing" isn't always a bug; sometimes it's a retention policy that already did exactly what it was configured to do.

Conclusion

Field auditing is like insurance — you don't think about it until you need it. Whether it's for IRM, security, or HR, making sure your changes are properly tracked is a must. Some of the ways it breaks are careless code; others are the platform working exactly as designed, just in a way that surprises anyone who hasn't hit it before. Knowing the difference is most of the battle — avoid these nine pitfalls, and your ServiceNow platform gets meaningfully more transparent, reliable, and audit-ready.