02 Get your data in /Integrations
ServiceNow Customer Service Management
Send ServiceNow Customer Service Management cases and their customer-visible comments to Dimension Labs with the universal API.
- SC ServiceNow Customer Service ManagementTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- Dimension LabsRead in full · enriched daily
This brings in each ServiceNow Customer Service Management (CSM) case as one conversation: the Additional comments on it, from the customer and from your agents. Work notes are internal and stay out. You run it on your side: a small script pulls from the ServiceNow Table API and posts each comment to the universal API.
What you need
Section titled “What you need”- A ServiceNow instance with Customer Service Management, and its name (the
yourinstanceinyourinstance.service-now.com). - A ServiceNow admin, to create an integration user.
- An integration user that can read the Case (
sn_customerservice_case), Journal Entry (sys_journal_field) and Contact (customer_contact) tables. The Table API returns only what this user’s roles and access controls allow, so your admin picks the roles. - A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in ServiceNow
Section titled “1. Get API access in ServiceNow”- Have your admin create the integration user and give it read access to the three tables above.
- Authenticate with Basic auth, using that user’s user name and password. The Table API examples use
--user 'username':'password'(Table API). - Check the user can read journal entries:
curl -u "$SN_USER:$SN_PASSWORD" -H "Accept: application/json" \ "https://yourinstance.service-now.com/api/now/table/sys_journal_field?sysparm_query=element=comments&sysparm_limit=1"Comments and work notes aren’t columns on the case. ServiceNow keeps each entry as its own row in sys_journal_field: element_id is the sys_id of the case and element is the journal field, comments for Additional comments (retrieving journal entries). The page above doesn’t name the column that holds the comment text. The response lists every column of the row: find the one holding the comment (on most instances it is value), and set SN_JOURNAL_TEXT_FIELD to its name.
2. Pull new cases
Section titled “2. Pull new cases”List cases updated since your cursor, then each case’s comments:
curl -u "$SN_USER:$SN_PASSWORD" -H "Accept: application/json" -G \ "https://yourinstance.service-now.com/api/now/table/sn_customerservice_case" \ --data-urlencode 'sysparm_query=sys_updated_on>=javascript:gs.minutesAgo(60)^ORDERBYsys_updated_on' \ --data-urlencode 'sysparm_fields=sys_id,number,state,priority,account,contact' \ --data-urlencode 'sysparm_exclude_reference_link=true' \ --data-urlencode 'sysparm_limit=1000'gs.minutesAgo(n)returns the UTC timenminutes ago, so the cursor needs no time zone (GlideSystem). The code turnssinceinto minutes.- Page with
sysparm_limitandsysparm_offset, or follow therel="next"URL in theLinkresponse header. ServiceNow applies the limit before access controls, so a page can come back short: don’t stop on a short page. - Dates come back in UTC, as
yyyy-MM-dd HH:mm:ss. Leavesysparm_display_valueoff: display values use the user’s time zone and cost performance.
For each case, get its comments: GET /api/now/table/sys_journal_field?sysparm_query=element_id={case sys_id}^element=comments^ORDERBYsys_created_on. Asking for element=comments leaves out work notes.
Rate limits: ServiceNow lets admins set hourly rate limit rules for inbound REST requests, by user, role or for everyone. A request over the limit returns 429 with a Retry-After header in seconds (inbound REST API rate limiting).
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | ServiceNow | Notes |
|---|---|---|
text | journal entry, column SN_JOURNAL_TEXT_FIELD | Only element=comments (Additional comments). Work notes are never requested. |
userId | case contact | The contact’s sys_id, on every comment of the case. |
incoming | journal sys_created_by equals the contact’s user_name | true for the contact’s comments, false for everyone else’s. |
sessionId | case number | Prefix it, for example servicenow-CS0001234. |
conversationId | case number | Same value as sessionId. |
dimensionlabs_timestamp | journal sys_created_on | UTC without an offset. toMillis reads it as UTC. |
platformJson | case state, priority, account | account is the customer account’s sys_id: use it to join to account data. |
The code skips cases with no contact (for example consumer cases), because it can’t tell the customer’s comments from your agents’ without one. Add the consumer’s user name the same way if you handle consumer cases.
Personal data. Don’t send the contact’s name, email or user name. Comments often hold names, addresses and phone numbers: redact them before you send. See PII redaction.
4. Send it
Section titled “4. Send it”// servicenow.mjs. Environment: SN_INSTANCE (yourinstance), SN_USER, SN_PASSWORD,// SN_JOURNAL_TEXT_FIELD (the journal column that holds the text, see step 1), DL_API_KEY.import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const base = `https://${process.env.SN_INSTANCE}.service-now.com/api/now/table`;const auth = 'Basic ' + Buffer.from(`${process.env.SN_USER}:${process.env.SN_PASSWORD}`).toString('base64');const TEXT = process.env.SN_JOURNAL_TEXT_FIELD;const ref = (v) => (v && typeof v === 'object' ? v.value : v) || '';
// Retries ServiceNow's rate limits (429) and server errors (5xx), honoring Retry-After.async function get(url) { for (let attempt = 0; ; attempt++) { const res = await fetch(url, { headers: { Authorization: auth, Accept: 'application/json' } }); if (res.ok) return { body: await res.json(), link: res.headers.get('link') ?? '' }; if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`ServiceNow returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
// Every row of a Table API query, following the rel="next" link.async function list(table, query, fields) { const rows = []; let url = `${base}/${table}?sysparm_query=${encodeURIComponent(query)}&sysparm_fields=${fields}&sysparm_exclude_reference_link=true&sysparm_limit=1000`; while (url) { const page = await get(url); rows.push(...page.body.result); url = /<([^>]+)>;\s*rel="next"/.exec(page.link)?.[1] ?? null; } return rows;}
// Sends every Additional comment written at or after `since` (Unix ms). Returns the new `since`.export async function syncServiceNow({ since, historical = false }) { const started = Date.now(); if (!TEXT) throw new Error('Set SN_JOURNAL_TEXT_FIELD to the sys_journal_field column that holds the comment text (step 1).'); const minutes = Math.ceil((Date.now() - since) / 60000) + 1; const cases = await list('sn_customerservice_case', `sys_updated_on>=javascript:gs.minutesAgo(${minutes})^ORDERBYsys_updated_on`, 'sys_id,number,state,priority,account,contact'); const contacts = new Map(); let newest = since; for (const c of cases) { const contact = ref(c.contact); if (!contact) continue; // no contact: the sides can't be told apart if (!contacts.has(contact)) contacts.set(contact, (await list('customer_contact', `sys_id=${contact}`, 'user_name'))[0]?.user_name ?? ''); const userName = contacts.get(contact); if (!userName) continue; const comments = await list('sys_journal_field', `element_id=${c.sys_id}^element=comments^ORDERBYsys_created_on`, `sys_id,sys_created_on,sys_created_by,${TEXT}`); for (const j of comments) { const ts = toMillis(j.sys_created_on); const text = String(j[TEXT] ?? '').trim(); if (ts < since || !text) continue; await sendToDimensionLabs({ text, userId: contact, incoming: j.sys_created_by === userName, sessionId: `servicenow-${c.number}`, conversationId: `servicenow-${c.number}`, dimensionlabs_timestamp: ts, platformJson: { case_sys_id: c.sys_id, state: c.state, priority: c.priority, account: ref(c.account) }, }, { historical: historical || ts < Date.now() - HOUR }); newest = Math.max(newest, ts); } } // A record updated mid-run can shift the paging; stopping the cursor a little before the run started means the next run reads it. Re-sends are safe. return Math.max(since, Math.min(newest, started - 5 * 60 * 1000));}The case query reaches back a minute past since, so a comment at the boundary can be sent twice. That’s fine: duplicates are removed automatically.
5. Run it
Section titled “5. Run it”Save the code as servicenow.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncServiceNow as sync } from './servicenow.mjs';.
- History first:
node run.mjs 2025-01-01sends every Additional comment on cases updated since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then every 15 minutes: the scheduled job sends new comments to
/track, and they are enriched automatically every day. A comment that is more than an hour old when the job sees it goes to/trackhistorical, and needs a backfill. - Re-runs are safe: duplicates are removed automatically.
Each case costs at least one extra request, more when its contact is new to the run, so check the job against any rate limit rule your admin has set. You can also upload a file with one row per comment (check it with the file checker), then run a backfill.
ServiceNow reference
Section titled “ServiceNow reference”- Table API
- Retrieve journal entries
- Journal fields
- Tables installed with Customer Service Management
- Operators available for filters and queries
- GlideSystem
- Inbound REST API rate limiting
Checked against ServiceNow’s documentation on 2026-10-26.