Are You Paying for ITIL Licenses Nobody Uses?
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 hours ago - last edited an hour ago
Overview
If you've managed ITIL licensing in ServiceNow long enough, you've probably asked some version of this question: how many of my ITIL holders do ITIL work? Did we create groups where only a handful of the individuals in that group are doing meaningful ITIL related work? How are we being good stewards to our organization by ensuring we are only purchasing the licenses we really need. It's a recurring topic on this forum and elsewhere, and most existing answers, including a few well-regarded community scripts share a similar shape: walk sys_user_has_role, check each holder's last_login or look for activity on the task table, output a list. ServiceNow even has a playbook, albeit for a different product, that follows the above-mentioned pattern.
Those patterns get you close, but each has a blind spot in a real environment:
- last_login flags daily readers as "active." Plenty of people log in every day to read incidents, browse change records, or approve things from the portal, none of which requires an ITIL license. If your audit treats them as active, they will survive every cycle while never touching a record that warranted the license.
- Querying the task table catches the wrong activity too. Catalog items submitted by Employee Service Center users land in sc_req_item, which extends task. Approvals leave traces. So does basic record viewing for users with overlapping roles. A blanket task query masks the inactive itil holders you're trying to surface.
I've been working on a quarterly audit that addresses these gaps. The MVP is in place, and I want to share the design decisions and where the work might be heading from here.
My design decisions:
Invert the Query
Instead of walking each ITIL holder and asking, "what have you done lately?", walk the sys_audit table for common non-end-user/administrator ITIL tables once and ask "who has touched anything?" Then subtract those users from the ITIL holders.
var activeUsers = {};
var auditGA = new GlideAggregate('sys_audit');
auditGA.addQuery('sys_created_on', '>=', cutoffDate);
auditGA.addQuery('tablename', 'IN', OPERATIONAL_TABLES.join(','));
auditGA.groupBy('user');
auditGA.query();
while (auditGA.next()) {
var auditUser = auditGA.getValue('user');
if (auditUser) activeUsers[auditUser] = true;
}
Why did I use sys_audit instead of sys_updated_by? My first version of this script aggregated on sys_updated_by on each fulfiller table, which seemed cleaner. It produced incorrect results. sys_updated_by reflects only the most recent updater of a record. When a workflow runs as system (for example, auto-close shifting an incident from Resolved to Closed), it overwrites the resolver's username and that legitimate fulfiller work becomes invisible. Some of the most productive ITIL users, the ones who resolve incidents cleanly and never touch the record again, were being misclassified as inactive. sys_audit captures every audited field change with the user who made it, so the resolver remains attributable regardless of what touches the record afterward.
Why inverting matters at scale. GlideAggregate with groupBy returns distinct values without dragging back full rows. One aggregate against sys_audit with the right filters runs in seconds. The per-user-loop pattern requires one GlideRecord query per holder per table, which would be roughly 3,600 queries for 600 holders across 6 tables. I.e. O(holders*tables). The inverted pattern is one aggregate query per table, regardless of how many holders you have, plus a fast hashmap lookup per holder. The difference compounds as the holder count grows.
Be deliberate about which tables I counted as "fulfiller work"
This is the design choice that separates a useful audit from a noisy one. Activity should only count on tables where a fulfiller license is genuinely required for the work being done.
My list:
- incident
- sc_task
- change_request
- change_task
- problem
- kb_knowledge
Tables I deliberately excluded:
- sc_req_item: Touched by users submitting catalog items from the portal. Not fulfiller work.
- sysapproval_approver: Approvers don't need fulfiller licenses.
- task directly: Too broad, includes way too much.
The narrow list will produce a few false positives (a user who legitimately only works one type of record outside this set), but it will not silently mask unused licenses. I would rather flag someone for outreach unnecessarily than have a license sit unused because the audit didn't see their inactivity. I devised a method for dealing with these special use case individuals, which I will elaborate on later.
Classify the grant path, because removal depends on it
When a user surfaces as inactive, my next question was "how did they get the role in the first place and what was the business logic behind it?” The grant path determines what action could be taken. Three categories, three different removal paths:
- Direct: Remove the role from user.
- Group: The user got ITIL through group membership. The reviewer needs to either remove the user from the group (which has side effects on assignment and notifications) or remove the role from the group itself (changes everyone's access).
- Role-Inherited: The user holds another role (like admin) that contains itil. The reviewer needs to cautiously review the containing role assignment instead.
Identifying how each user holds the role is harder than it should be, the platform's built-in fields don't reliably surface whether a grant is direct, via group, or via another role that contains it. The script reconstructs this from the underlying group membership and role-containment tables. The result is that each user on the report carries a label like Direct, Group: <group name>, or Role-Inherited (via <role name>), so the reviewer knows exactly what they're working with before reaching out.
A report that says "remove itil from this user" without telling the reviewer it's role-inherited produces confusion. The reviewer thinks they removed it; the next role recalculation puts it back; the audit flags the same user next quarter. Categorization makes the remediation much clearer and leads to action that works.
Example:
Name | Department | Grant Type |
Ricky Carmichael | IT Operations | Direct |
Travis Pastrana | Service Desk | Group: FIM World Champions |
Jeremy McGrath | IT Operations | Group: Enduro |
James Stewart | Platform Engineering | Role-Inherited (via admin) |
Stefan Everts | Network Engineering | Group: FIM World Champions, Role-Inherited (via admin) |
Prerequisites:
This pattern relies on sys_audit being populated for the fulfiller tables you're auditing. The standard task-extending tables (incident, sc_task, change_request, change_task, problem) have audit enabled by default in OOB ServiceNow, so most instances are ready to go. kb_knowledge is the table you might want to double-check as it doesn't extend task and audit isn't universally on for it by default. You can check it directly by going to the system dictionary and looking if audit is true. (All > sys_dictionary) or run this in scripts background in one of your non production instances:
var tables = ['incident', 'sc_task', 'change_request', 'change_task', 'problem', 'kb_knowledge'];
for (var i = 0; i < tables.length; i++) {
var t = new GlideRecord('sys_db_object');
t.addQuery('name', tables[i]);
t.query();
if (t.next()) gs.info(tables[i] + ': audit = ' + t.getValue('audit'));
}
If any of them return audit = 0, enable audit on those tables before running the script, or drop them from the table list. (With proper system administrative due diligence.) The script will only catch activity on tables that are being audited.
What the Report Contains:
Two HTML tables in the email body, plus a summary header.
The Action List: Users who are ITIL inactive AND not currently exempt. Columns: Name, Username, Email, Job Title, Department, Manager, Manager Email, Last Login, Grant Type(s). The reviewer has everything they need to send an outreach email without going back to the platform.
The Currently Exempt List: Users who would otherwise appear on the action list but are covered by an active exemption. Columns: Name, Username, Department, Reason, Expires On, Notes. Sorted by expiration ascending so soon-to-lapse exemptions surface first.
Showing both tables in the same report turned out to be more valuable than I expected. It moves the artifact from "list of people to contact" to "current state of license attestation," which is the framing leadership likes.
Example Report:
ITIL Role Inactivity Audit
Report Date: 2026-06-09 08:00:00
Inactivity Threshold: 90 days
Tables Audited: incident, sc_task, change_request, change_task, problem, kb_knowledge
Total Inactive ITIL Users (action required): 5
Inactive Users Currently Exempt: 2
Generated by the ITIL Inactivity Audit scheduled job. Activity is defined as any audited change to fulfiller-licensed tables within the last 90 days, sourced from sys_audit. Last Login is shown for context only and does not factor into the inactivity determination.
| Name | Username | Title | Department | Manager Name | Manager Email | Last Login | Grant Type(s) | |
| Ricky Carmichael | rcarmichael | rcarmichael@example.com | Senior Technician | IT Operations | David Bailey | dbailey@example.com | 2026-03-15 09:22 | Direct |
| Travis Pastrana | tpastrana | tpastrana@example.com | Service Desk Lead | Service Desk | Sarah Lopez | slopez@example.com | 2026-05-22 14:08 | Group: FIM World Champions |
| Jeremy McGrath | jmcgrath | jmcgrath@example.com | Systems Engineer | IT Operations | David Bailey | dbailey@example.com | Never | Group: Enduro Direct |
| James Stewart | jstewart | jstewart@example.com | Platform Architect | Platform Engineering | Maria Chen | mchen@example.com | 2026-02-08 11:47 | Role-Inherited (via admin) |
| Stefan Everts | severts | severts@example.com | Network Specialist | Network Engineering | Maria Chen | mchen@example.com | 2026-04-12 16:33 | Group: FIM World Champions Role-Inherited (via admin) |
Currently Exempt from Action
These users would otherwise appear on the action list above but are covered by an active exemption. No outreach is required. Exemptions past their expiration date are automatically dropped and the user re-enters the audit on the next run.
| Name | Username | Department | Reason | Expires On | Notes |
| Mark Thompson | mthompson | Application Support | Extended leave | 2026-08-30 | On medical leave through August, expected return date confirmed by HR. |
| Susan Hayes | shayes | Executive Leadership | Leadership | 2026-12-15 | VP approved low-volume use for quarterly governance review meetings. |
The MVP addition: an exemption list
Every audit needs a way to say, "this user is known to be inactive and that is fine." Without one, the same people show up on the report every cycle and reviewers either ignore the report or build informal "skip these" lists.
I built this as a custom table in the scoped app with the following key fields:
- User: Reference to sys_user
- Reason: Choice (leadership, extended leave, approved low-volume use…)
- Effective from: Date
- Expires on: Date
- Notes: Long string
- Active: True/False
- Source: Choice (manual). (I built this for possible Phase 2).
Two non-obvious decisions I made:
expires_on is required. Every exemption auto-falls off on a date, forcing periodic re-attestation rather than letting "temporary" exemptions become permanent. This is the single most important governance feature in design. Without it, the list becomes a dumping ground over time.
One active exemption per user, enforced by business rule. Inactive (expired) exemptions accumulate as history; only one active row is allowed at a time. The script honors only active + non-expired rows, so stale exemptions naturally re-enter the audit on the next cycle.
The audit script does one additional query at the start of its run to build a hashmap of currently exempt usernames, then skips them when assembling the action list.
The MVP and maybe more in the future
The Minimal Viable Product is a manual workflow with a quarterly report and an exemption table populated by admins. My full vision is a manager-driven, mostly automated process.
Phased:
- MVP (now): Exemption table + script honors it + manual maintenance.
- Catalog item for "Request ITIL audit exemption": Manager-facing record producer that creates the exemption record with proper attribution.
- Per-manager digest report: Slice the quarterly output so each manager sees only their direct reports, with grant-type-aware context for what action paths are available.
- Wire the digest emails to catalog items: "Retain" link pre-populates the exemption catalog item; "Remove" link routes to your existing group membership catalog (I do not want a one-click action that fires a flow blindly).
- Non-response escalation: Track unaddressed users across cycles and escalate to the manager's manager after N cycles.
I think each phase is independently shippable and useful.
What I'd value feedback on!
A few open questions where I think this community will have useful perspectives:
- The fulfiller-table list. Is there a table I should add or remove from the activity check?
- Non-response defaults. If I was to develop to phase 5 and a manager hasn't acted across multiple cycles, what should I do?
- Self-attestation. What's your pattern for handling the case where the user with inactive ITIL usage IS a manager whose own manager doesn't have ServiceNow context to make the call? Pre-populating exemptions for leadership is what I’m thinking about doing.
Happy to share more of the build if there's interest. I've thought about publishing it on ServiceNow Share. The pattern should be portable across orgs since none of the design depends on anything organization specific.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
an hour ago
I love this idea - I have been trying to do this for quite a long time - as we know that are folks just looking at tickets and not doing anything with them - that don't need licenses. Is there an update set your are publishing for this? Would love to try this out.