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 tosys_audit. This is the specific call responsible for the "missing" audit entry. If a change is made withsetWorkflow(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 stopssys_updated_on,sys_updated_by, andsys_mod_countfrom 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 Tofield. - 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_exceptiontable forsetWorkflow(false)orautoSysFields(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 anygs.info()orgs.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.
No comments:
Post a Comment