// Platform Engineering · Six Years Deep

ServiceNow
Architecture

The undocumented corners of the platform. Production-proven patterns in API design, integration architecture, authentication engineering, and upgrade governance.

// 01

Specialisms

001
API Architecture & Dynamic Generation
Scripted REST frameworks that register endpoints at runtime from configuration records. OpenAPI/Swagger-first delivery, tenant-aware response shaping, non-developer API onboarding.
002
Composite Authentication Design
Layering Bearer tokens, mTLS, and HMAC-signed headers on single outbound calls. Custom OAuth exchanges, JWT validation, Connection & Credential Alias architecture.
003
Transactional API Engineering
Multi-step APIs with staging, transformation, orchestration, and rollback for provisioning, diagnostics, and financial workflows. Async callbacks and downstream failure handling.
004
ACL-Driven Payload Filtering
Using the ACL engine to dynamically strip response fields by calling principal — multi-tenant data segregation without forking endpoint logic or adding middleware.
005
Scoped Application Development
End-to-end scoped app architecture: data model, business logic, Service Portal widgets in AngularJS, Flow Designer orchestration, cross-scope API exposure. ISO 27001 and GDPR aligned.
006
Upgrade Governance & Regression
Structured pre-upgrade regression protocols, post-upgrade certification, and platform communication strategy. Authored a process still in use after departure — that's the benchmark.
// 02

Code Playground

Runtime Endpoint Registration from Config Records
Scripted REST Resource registered dynamically — no REST API UI required
// API Registry table: u_api_registry
// Fields: u_active, u_path, u_handler, u_auth_type, u_acl_role
// Called from a Script Include on activation

function registerAPIEndpoints() {
  var registry = new GlideRecord('u_api_registry');
  registry.addQuery('u_active', true);
  registry.query();

  while (registry.next()) {
    var path    = registry.u_path.toString();
    var handler = registry.u_handler.toString();
    var authType= registry.u_auth_type.toString();
    var aclRole = registry.u_acl_role.toString();

    createScriptedResource(path, handler, authType, aclRole);
  }
}

function createScriptedResource(path, handler, authType, aclRole) {
  // Check if resource already exists
  var existing = new GlideRecord('sys_ws_endpoint');
  existing.addQuery('relative_path', path);
  existing.query();
  if (existing.next()) return; // already registered

  var endpoint = new GlideRecord('sys_ws_endpoint');
  endpoint.newRecord();
  endpoint.setValue('relative_path', path);
  endpoint.setValue('script_include', handler);
  endpoint.setValue('requires_authentication', authType !== 'none');
  endpoint.setValue('required_role', aclRole);
  endpoint.insert();
  gs.log('API endpoint registered: ' + path);
}
This pattern allows non-developers to manage API endpoints via a UI form rather than touching code. The registry table acts as configuration; the registration script runs on app activation or via a scheduled job. Combined with ACL-driven payload filtering (see tab 4), you get a fully configurable, secure API layer with no code changes per consumer.
Composite Authentication: Bearer + mTLS + HMAC Signature
All three auth mechanisms on a single outbound REST call
// Pre-requisites:
// - Bearer token retrieved from credential store or token endpoint
// - mTLS alias configured in Connection & Credential Aliases
// - HMAC secret stored in sys_properties (encrypted)

function callSecureEndpoint(endpoint, payload) {
  var r = new sn_ws.RESTMessageV2();
  r.setEndpoint(endpoint);
  r.setHttpMethod('POST');
  r.setRequestBody(JSON.stringify(payload));
  r.setHttpHeader('Content-Type', 'application/json');

  // Layer 1: Bearer token
  var token = getBearerToken(); // your token retrieval logic
  r.setHttpHeader('Authorization', 'Bearer ' + token);

  // Layer 2: HMAC-signed request header
  var secret = gs.getProperty('u_api_hmac_secret');
  var timestamp = new GlideDateTime().getNumericValue().toString();
  var signature = computeHMAC(
    JSON.stringify(payload) + timestamp, secret
  );
  r.setHttpHeader('X-Timestamp', timestamp);
  r.setHttpHeader('X-Signature', signature);

  // Layer 3: mTLS via MID Server keystore alias
  r.setMutualAuth('u_mtls_client_alias');

  var response = r.execute();
  if (response.getStatusCode() !== 200) {
    gs.logError('Secure endpoint call failed: '
      + response.getStatusCode()
      + ' ' + response.getBody());
    return null;
  }
  return JSON.parse(response.getBody());
}

function computeHMAC(message, secret) {
  // Use GlideDigest for HMAC-SHA256
  var digest = new GlideDigest();
  return digest.getSHA256HMACBase64(secret, message);
}
The critical ordering: set the request body before computing the HMAC signature, since the signature covers the payload. The setMutualAuth() call references a Connection & Credential Alias configured with the client certificate — the MID Server handles the TLS handshake. This combination is required by some high-security financial and government integrations that won't accept any single auth mechanism alone.
Safe Reference Resolution in Async Business Rules
The getRefRecord() async context trap — and the fix
// Problem: getRefRecord() in async context may return stale data
// The GlideElement snapshot captured at queue time carries cached
// display values that diverge from the committed database state.

// ✕ WRONG — unreliable in async Business Rules
var refRecord = current.assignment_group.getRefRecord();
var manager = refRecord.manager.toString(); // may be stale

// ✓ CORRECT — explicit re-query via sys_id
// toString() on a reference field reliably returns the sys_id
// from the snapshot — only the cached display values are stale.

var groupSysId = current.assignment_group.toString();

if (groupSysId) {
  var group = new GlideRecord('sys_user_group');
  if (group.get(groupSysId)) {
    // Fresh database read — always correct
    var manager = group.manager.toString();
    var groupName = group.name.toString();

    // Additional dot-walk? Use another explicit query.
    if (manager) {
      var managerRec = new GlideRecord('sys_user');
      if (managerRec.get(manager)) {
        var email = managerRec.email.toString();
      }
    }
  }
}
This bug is silent and intermittent — it only manifests under load when there's lag between queue time and async worker execution. The rule: in async context, treat current as a bag of sys_ids and primitive values only. Never call getRefRecord(), never dot-walk, never call getDisplayValue() on reference fields. Read the full article →
ACL-Driven Payload Filtering by Calling Principal
Strip response fields dynamically based on consumer role — no endpoint forks
// In your Scripted REST Resource handler
// This pattern enforces field-level access control at the API layer

(function process(request, response) {

  // Build the full response object
  var record = new GlideRecord('incident');
  if (!record.get(request.pathParams.sys_id)) {
    response.setStatus(404);
    return;
  }

  var payload = {
    sys_id:       record.sys_id.toString(),
    number:       record.number.toString(),
    short_desc:   record.short_description.toString(),
    state:        record.state.toString(),
    // Sensitive fields — conditionally included
    caller_id:    record.caller_id.toString(),
    work_notes:   record.work_notes.toString(),
    cost_center:  record.u_cost_center.toString()
  };

  // Determine caller identity from header or OAuth token
  var callerSysId = request.getHeader('X-Caller-SysId');

  // Strip fields based on caller's roles
  if (!callerHasRole(callerSysId, 'u_api_sensitive_data')) {
    delete payload.caller_id;
    delete payload.work_notes;
    delete payload.cost_center;
  }

  response.setContentType('application/json');
  response.setBody(JSON.stringify(payload));

})(request, response);

// Helper — checks if a user sys_id has a given role
function callerHasRole(userSysId, roleName) {
  var roleCheck = new GlideRecord('sys_user_has_role');
  roleCheck.addQuery('user', userSysId);
  roleCheck.addQuery('role.name', roleName);
  roleCheck.addQuery('state', 'active');
  roleCheck.query();
  return roleCheck.hasNext();
}
One endpoint, multiple consumers with different visibility — no forked logic. The consumer's identity is resolved from the request header (or OAuth token), their roles are checked, and sensitive fields are deleted from the payload object before serialisation. This is more robust than maintaining separate endpoint versions and ensures field-level access control is consistently enforced at the API boundary rather than relying on consumers to self-enforce.
Safe Dot-Walk Replacement for Async Context
Breaking reference chains into explicit queries
// Dot-walking works fine in sync Business Rules.
// In async context, each reference hop may resolve stale data.
// Replace dot-walk chains with sequential explicit GlideRecord queries.

// Goal: get the email of the manager of the incident's assignment group

// ✕ WRONG in async — each dot is a potential stale reference
// var email = current.assignment_group.manager.email.toString();

// ✓ CORRECT — explicit chain with null checks at each step
var email = getGroupManagerEmail(current.assignment_group.toString());

function getGroupManagerEmail(groupSysId) {
  if (!groupSysId) return null;

  var group = new GlideRecord('sys_user_group');
  if (!group.get(groupSysId)) return null;

  var managerSysId = group.manager.toString();
  if (!managerSysId) return null;

  var manager = new GlideRecord('sys_user');
  if (!manager.get(managerSysId)) return null;

  var email = manager.email.toString();
  return email || null;
}

// This pattern is:
// - Safe in both sync and async context
// - Explicit and debuggable (each step can be logged)
// - Null-safe at every reference boundary
// - Slightly more verbose — worth it every time
The verbose version is always the correct one in async context. Encapsulate the chain in a helper function so the calling code stays clean. Each explicit GlideRecord.get() is a fresh database read — immune to snapshot staleness. Include null checks at every step: in async, it's entirely possible for a reference that existed at queue time to have been modified or deleted by execution time.
// 03

Articles

Platform Internals
The GlideRecord Async Context Trap — and How to Fix It
When a Business Rule runs in an async worker thread, getRefRecord() silently returns stale data. Why it happens, how to detect it, and the pattern that fixes it reliably.
~10 min
Read →
API Design
Auto-Generating REST APIs from Configuration Records
Building a framework that reads from a custom table and dynamically registers Scripted REST Resources at runtime — no code deployments required.
~12 min
COMING SOON
Auth & Security
Composite Auth: Bearer + mTLS + Signed Headers in a Single Request
The undocumented interaction between Connection & Credential Aliases and custom MID Server keystores — and how to combine all three auth mechanisms.
~14 min
COMING SOON
Upgrade Governance
The Upgrade That Silently Changed My Business Rules
A Washington-to-Xanadu post-mortem. Three business rules stopped working with no error, no skip log, no warning. How to build a regression harness that catches this.
~9 min
COMING SOON
// 04

Platform Background

Senior ServiceNow Developer
Oct 2023 – Present
Capgemini · ServiceNow Practice
Led complex ITSM, CSM, and bespoke scoped application implementations across public and private sector clients.
Ensured ISO 27001 and GDPR compliance across all integration work; contributed to pre-sales solutioning and client demonstrations.
ServiceNow Developer & Administrator
Oct 2019 – Oct 2022
Fieldfisher · International Law Firm
Sole technical owner of the ServiceNow platform for 800+ staff — full end-to-end responsibility with no team, no backup, and no internal precedent.
Built scoped applications, workflows, business rules, and integrations; managed instance security architecture and access governance.
Authored the upgrade regression protocol that covered multiple major ServiceNow releases — still in active use after departure.