The Problem
We were building a floating chat widget for ServiceNow's Service Portal (/sp). The widget was deployed as a sys_ui_script record with global = true and active = true — exactly what the documentation implies should inject JavaScript across all pages.
It didn't load. The widget never appeared.
Fix 1: sys_ui_script Does Not Execute in Service Portal
Symptom: Widget never appears at /sp. No JS errors. No window.__aiSvcChat variable. document.getElementById('ai-svc-root') returns null.
Diagnosis: Open browser DevTools on /sp and run:
window.__aiSvcChat // returns undefined — script never ran
document.getElementById('ai-svc-root') // returns null — element never mounted
Root cause: sys_ui_script with global = true is designed for the classic ServiceNow UI (nav_to.do, list views, form views). Despite the name, it does not execute in Service Portal's AngularJS rendering context.
Fix: Use the correct Service Portal injection mechanism — create an sp_instance record placing your widget on the portal's home page. A widget with position: fixed CSS floats over the entire portal regardless of where in the page grid it's placed.
sp_portal (url_suffix=sp)
→ sp_page (id=index)
→ sp_container → sp_row → sp_column
→ sp_instance (sp_widget = your widget sys_id, order = 100)
This is the correct SP mechanism. The widget renders once when the Angular SPA loads and persists across page navigation.
Fix 2: RESTAPIResponse.setBody() Rejects Pre-Stringified JSON
Symptom: Every POST to the Scripted REST API returns HTTP 500 with a Rhino JS type coercion error in the system log.
Root cause: The operation script called response.setBody(JSON.stringify(payload)). ServiceNow's RESTAPIResponse.setBody() expects a plain JavaScript object — it performs the serialization internally. Passing a pre-stringified string triggers a type error in the Rhino JS engine.
Fix:
// Wrong
response.setBody(JSON.stringify({ answer: result, suggestions: [] }));
// Correct
response.setBody({ answer: result, suggestions: [] });
Remove every JSON.stringify() wrapper from setBody() calls. This applies to all Scripted REST API operation scripts.
Fix 3: ServiceNow Auto-Wraps setBody() Responses in a result Key
Symptom: API returns HTTP 200. Client reads response.data.answer — gets undefined. The response body is present but the field isn't where you expect it.
Root cause: ServiceNow's Scripted REST API automatically wraps setBody() output in a result key. The actual shape is { result: { answer: "...", suggestions: [...] } }, not { answer: "...", suggestions: [...] }.
Fix: Use a defensive read pattern on the client:
var data = response.data.result || response.data;
// Now data.answer is correct regardless of wrapper presence
This handles both the wrapped and unwrapped shape, making the client resilient to future ServiceNow version changes.
Fix 4: Widget Suggestions Rendering Beside Text Instead of Below
Symptom: Catalog suggestion cards appear to the right of the bot response text, not below it. The chat panel looks broken — text and cards side by side in a cramped layout.
Root cause: The HTML structure placed the suggestion cards as a sibling of the response bubble inside a display: flex row container:
Fix: Wrap the bubble and suggestions in a column-flex container:
.ai-bot-content {
display: flex;
flex-direction: column;
max-width: 330px;
}
Fix 5: Claude Leaks sys_id Values When Told to Use Record Numbers
Symptom: Claude references catalog items as Reclaim Asset (REQ0001297d75ba...) — fabricating a REQ-style number from the catalog item's sys_id, which it received in the system prompt context.
Root cause: The system prompt said "use record numbers (e.g. INC0001234)" without distinguishing between records that have numbers (incidents, requests) and catalog items that don't. Claude tried to apply the instruction uniformly and hallucinated a number from the sys_id it had available.
Fix: Be explicit about the distinction in the system prompt:
When referencing incidents use their INC number (e.g. INC0001234).
When referencing requests use their REQ number (e.g. REQ0000001).
Catalog items do not have record numbers — reference them by name only
(e.g. "Standard Laptop"). Never mention or reveal internal sys_id values.
Verified clean in retesting: no fabricated numbers, catalog items referenced by name only.
Summary
| Problem | Root Cause | Fix |
|---|---|---|
| Widget never loads at /sp | sys_ui_script doesn't execute in SP | Use sp_instance on SP home page |
| HTTP 500 on all API calls | setBody() rejects JSON.stringify | Pass plain JS object, not string |
| response.data.answer is undefined | SN wraps setBody in result key | Read data.result \|\| data |
| Suggestions appear beside text | flex siblings in row layout | Wrap in column-flex div |
| sys_id values in Claude output | Prompt ambiguity for catalog items | Explicit per-type instructions |