Showing posts with label Field Audit. Show all posts
Showing posts with label Field Audit. Show all posts

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.

Wednesday, June 18, 2025

The Case of the Missing Audit Log: Debugging Unexpected Field Updates in ServiceNow IRM

Introduction

Audit history is one of ServiceNow's most powerful features — especially in compliance-heavy environments like Integrated Risk Management (IRM). But what happens when a field update appears in audit logs, and yet the actual value in the record is something else entirely?

In this article, we dive into a real-world debugging experience where the Valid To field on a Policy Exception record showed unexpected behavior:

  • Audit history said one thing.
  • The actual value in the record said another.
  • And there was no log of the second change at all.

Let's unpack the mystery, walk through how to debug and fix it — and along the way, correct a step that shows up in a lot of similar debugging guides but actually leads nowhere for this specific problem.

The Scenario

Imagine this:

  • A workflow sets the Valid To date based on an extension request.
  • The audit log correctly records the update: Valid To changed from 2024-06-01 to 2025-06-01
  • But when the user opens the record, the value is 2025-07-01.
  • And there's no second audit trail showing that change at all.

Spooky? Not really. Here's why it happens — and how to fix it.

Common Root Causes

1. Audit Suppressed in a Second Update

Most often, a second script or flow step updates the field using code like:

gr.setWorkflow(false);
gr.autoSysFields(false);
gr.setValue('valid_to', '2025-07-01');
gr.update();

It's worth being precise about what each of these two calls actually does here, because they're doing two different jobs, not one:

  • setWorkflow(false) disables the business rules that would normally fire on this update — and that includes the platform logic responsible for writing to sys_audit. This is the specific call responsible for the "missing" audit entry. If a change is made with setWorkflow(false) set, there is no audit record of it, full stop — not a suppressed-but-recoverable entry, just nothing written at all.
  • autoSysFields(false) is a separate concern: it stops sys_updated_on, sys_updated_by, and sys_mod_count from updating. This means the record won't even show up if someone sorts a list view by "Updated" looking for recent changes — the record looks untouched by every visible signal, not just missing from the audit related list specifically.

Together, these two calls make a change close to invisible through normal means: no audit entry, no updated timestamp, no bumped modification count. This pattern is commonly used — and genuinely risky — in scripted fixes or back-end updates, which is exactly why it's worth treating as a flag during code review on any table where change history matters.

2. Parallel Workflow Paths or Subflows

In Flow Designer, multiple paths may be active on the same record:

  • One branch sets the expected value.
  • Another runs later and overwrites it silently.

These steps can conflict if timing and conditions aren't carefully managed — and unlike the scripted case above, this doesn't require anyone to have deliberately suppressed anything. Two legitimate paths, each individually correct, can still produce a confusing outcome together.

3. Custom Business Rules or Fix Scripts

An after update Business Rule might be listening for something like extension_granted = true, and then adjusting valid_to automatically — possibly without whoever's debugging the issue even knowing that rule exists. On a well-established instance, tables like this can accumulate business logic across years and multiple teams, and no single person necessarily has the full picture of everything that reacts to a given field change.

How to Diagnose the Issue

Step 1: Confirm Audit Settings

  • Go to System Definition > Dictionary.
  • Find the Valid To field.
  • Make sure Audit = true.

If auditing isn't even enabled on the field, none of this is a mystery — it's expected behavior, and the fix is simply turning auditing on.

Step 2: Add a Temporary Debug Business Rule

Create a rule on sn_compliance_policy_exception:

(function executeRule(current, previous) {
   if (current.valid_to != previous.valid_to) {
      gs.info("[Audit Debug] Valid To changed: " + previous.valid_to + " → " + current.valid_to);
   }
})(current, previous);

This catches silent or unexpected changes going forward — but it's worth being clear about what it can and can't do. It will reliably catch the next occurrence, since a business rule you add yourself still fires regardless of setWorkflow(false) being called elsewhere (that call suppresses other business rules on the same transaction, not future rules you add going forward — though note it won't catch a case where setWorkflow(false) genuinely disables all business rule execution for that specific transaction; test this against your actual scenario). It cannot retroactively tell you what already happened in the past — for that, you're dependent on whatever trace the responsible script left elsewhere, which brings us to the next step, and where a very commonly cited debugging step doesn't actually apply here.

Step 3: Don't Reach for sys_update_xml — Here's Why, and What to Check Instead

A lot of ServiceNow debugging advice reflexively points to sys_update_xml when a change seems to have "gone missing." For this specific problem, it won't help, and it's worth understanding why: sys_update_xml tracks changes to tables that have the update_synch dictionary attribute set to true — these are configuration/metadata tables meant to travel through Update Sets (business rules, client scripts, UI policies, and similar). A Policy Exception record is business data, not platform configuration, and its table isn't tracked this way. Filtering sys_update_xml by sn_compliance_policy_exception will come back empty, not because nothing happened, but because that table was never going to record data changes like this in the first place.

What to check instead:

  • Search the responsible code directly, not the data. Look through Business Rules, Script Includes, and Flow Designer actions that reference the sn_compliance_policy_exception table for setWorkflow(false) or autoSysFields(false). This finds the script capable of causing the problem, even if it can't tell you exactly when it last ran.
  • Check the system log (syslog) for the affected time window. If the responsible script includes any gs.info() or gs.error() calls, this may be the only surviving trace of the actual event.
  • Use Flow Execution records (covered in Step 4) if a flow or subflow is a suspect, since Flow Designer keeps its own execution history independent of sys_audit.
  • Accept that some past occurrences may be genuinely unrecoverable. If setWorkflow(false) was used and nothing else logged the change, there may be no way to reconstruct exactly what happened after the fact. That's precisely why Step 2's debug business rule matters — it's not really about solving today's mystery, it's about making sure the next occurrence doesn't become an identical, unsolvable one.

Step 4: Trace Flow Designer Executions

Use Flow Execution records to:

  • Trace exactly which flow or subflow ran.
  • Identify timing conflicts or overwrite issues between parallel paths.

How to Fix It

Issue Fix
Script updates without audit Avoid setWorkflow(false) on compliance-relevant tables where possible; where it's genuinely necessary, pair it with manual logging so the change isn't invisible everywhere.
Subflow overwriting value Add guardrails — explicit conditions or mutually exclusive paths — so two branches can't both act on the same field.
Business Rule silently modifying value Log the source, and review all after update rules on the table as part of any related incident investigation, not just the one you already suspect.

Why This Matters More in IRM Specifically

This isn't just a debugging inconvenience — on a Policy Exception record specifically, it's a compliance concern in its own right. The whole point of ServiceNow's IRM architecture (Authority Documents → Citations → Control Objectives → Controls) is end-to-end traceability, and a Policy Exception's Valid To date is exactly the kind of field an auditor cares about later: it defines the window during which an organization has formally acknowledged it isn't meeting a control objective. An untracked change to that date doesn't just make debugging harder — it undermines the audit trail integrity that IRM exists to provide in the first place. On tables like this, treating setWorkflow(false) as a routine convenience is a bigger risk than it might be elsewhere in the platform.

Bonus: Set Up Audit Logging With an Explanation

Want to catch even stealthier changes going forward? Add a log field (u_valid_to_reason) that captures why a value was changed — manually, via workflow, or via script — at the point the change is made, rather than relying entirely on reconstructing intent after the fact. For compliance-relevant fields specifically, consider making this mandatory at the script level (reject the update if no reason was supplied) rather than optional, since an optional field tends to get skipped under exactly the time pressure that produces these silent-change incidents in the first place.

Conclusion

Unexpected field values with mismatched audit history are a red flag — especially in IRM and compliance workflows. Fortunately, with the right debugging steps, you can identify silent updates and take control over field integrity. Just be careful which steps you actually reach for: sys_update_xml is the right tool for tracking configuration changes across environments, but it has nothing to say about what happened to a single data record's field value.

Audit trails are only as good as the rules that protect them — so use script discipline, flow clarity, and logging best practices to make sure no change goes untracked, particularly on the tables where an auditor might eventually ask you to prove it.