Home / Question Hub / Portal & Catalog

Portal & Catalog

Service Portal widgets, the client-server data dance, catalog items, record producers and order guides.

No questions match your search.

Service Portal

16 questions

The building block of portal pages: a self-contained component with an HTML template (AngularJS), client controller, server script, and optional CSS and options schema. The server script builds a data object; the client controller binds it to the template.

The widget is the reusable definition (code); a widget instance is one placement of it on a page, with its own option values (title, filter, size). One "My Open Tickets" widget, five instances configured differently across portals.

Same page: AngularJS events — $rootScope.$broadcast('event.name', payload) in one controller, $scope.$on('event.name', fn) in the other. Also possible: a shared client-side service, or embedding one widget in another (<widget> tag / spUtil.get()) and passing options.

URL parameters — navigate to ?id=target_page&sys_id=...&key=value and read them in the target widget's server script via $sp.getParameter('key') (or $location.search() client-side). For heavier state, stash it server-side (user preference or record) and pass only the key.

// Server script
(function () {
    data.incidents = [];
    var grIncident = new GlideRecord('incident');
    grIncident.addActiveQuery();
    grIncident.setLimit(10);
    grIncident.query();
    while (grIncident.next()) {
        data.incidents.push({
            number: grIncident.getValue('number'),
            short_description: grIncident.getValue('short_description')
        });
    }
})();

// Client controller
function ($scope) {
    var c = this;   // c.data.incidents is ready to bind
}

Whatever the server script puts on data is serialized to the client as c.data. Push plain objects, never GlideRecords.

// Client
c.data.action = 'resolve';
c.data.sysId = incident.sys_id;
c.server.update().then(function () {
    // server has run; c.data is refreshed
});

// Server script — runs again on update()
if (input && input.action === 'resolve') {
    // input carries the client's data object
}

c.server.update() re-runs the server script with the client's data arriving as input. c.server.get({...}) is similar but passes an explicit payload and doesn't overwrite c.data.

  • $sp — server-side portal API (getParameter, getRecord, getWidget).
  • spUtil — client utility (update, addInfoMessage, recordWatch, get).
  • spModal — dialogs/prompts.
  • GlideSPScriptable, plus standard Angular services ($scope, $http, $location) and GlideAjax where needed.
// Client controller
spUtil.recordWatch($scope, 'incident', 'active=true', function (name, data) {
    spUtil.update($scope);   // re-run server script, re-render
});

spUtil.recordWatch() subscribes the widget to live updates on a table+filter over the AMB channel; the callback typically calls spUtil.update($scope) to refresh.

"Peak-only" points at capacity, not code: check node stats and semaphore exhaustion at those hours, slow query log for the widget's server script (missing setLimit/index?), transaction quota kills, and whether recordWatch storms multiply load with concurrent users. Reproduce with the widget's server script timed in a background script against production-scale data. Fix is usually query optimization + caching, not the widget HTML.

Client → HTML: put it on the controller (c.message = 'hi') and bind ({{c.message}}) — standard Angular. Server → client: the data object, arriving as c.data. The full loop: server data → client c.data → template bindings → user acts → c.server.update() → server input.

CSS that applies rules conditionally on viewport characteristics — @media (max-width: 768px) { ... } — the mechanism behind responsive widget layouts. In portal work you use them in widget CSS/theme to adapt cards, hide columns and resize type for mobile.

update() sends the whole c.data as input, re-runs the server script, and replaces c.data with the result. get({payload}) sends only what you pass and resolves with the response, leaving c.data untouched — better for one-off lookups that shouldn't reset widget state.

OOB portal Announcements support targeting via user criteria — create criteria matching the location(s) and attach to the announcement. Custom widget version: server script reads gs.getUser()'s location and queries a announcements table filtered on it.

Employee Center is ServiceNow's OOB portal product built on the Service Portal framework: multi-department (IT/HR/facilities) unified experience with topic taxonomy, curated content, campaigns — configured rather than built. Service Portal is the underlying framework you build any custom portal with. New employee-facing experiences default to EC; custom widgets still work inside it.

Each portal has its own URL suffix (/sp, /esc, /vendor) with login redirect handled by script — an installation exit or login redirect rule sending users to a portal by role. Within one portal: page-level role restrictions and widget-instance user criteria shape what each persona sees.

The widget definition: HTML template, client controller, server script, CSS, option schema and dependencies get duplicated to your editable copy — cloning is how you customize a read-only OOB widget. Not copied: the original's instances (pages keep pointing at the original until you swap them) and future OOB upgrades (your clone is frozen — the trade-off to mention).

Catalog Items

19 questions

An orderable offering in the service catalog — "New laptop", "VPN access" — with a form of variables, pricing/approval settings, and a fulfillment process (flow/workflow) that runs when ordered. Ordering one produces the REQ → RITM → task record chain.

Catalogs (the storefronts, e.g. IT vs HR), categories (the aisles), items (the products, incl. record producers and order guides), and variables/variable sets (the form each item asks). Fulfillment automation is the engine behind them.

A catalog item creates the request chain (REQ/RITM/tasks) and runs fulfillment. A record producer creates a record directly on any table you choose — incident, HR case — from a friendly catalog-style form. Want fulfillment tracking? Item. Want a nice front-end for creating a record? Producer.

Reuse and consistency: define a group of variables once (e.g. "Employee details"), attach to many items, maintain in one place — including its own catalog UI policies and client scripts that travel with it. Multi-row variable sets additionally capture table-like input (N rows of the same fields).

sc_request (REQ) — the order envelope, one per checkout; sc_req_item (RITM) — one per item ordered, carries the variables and drives fulfillment; sc_task — the work units created by the RITM's flow for fulfillers. Cart with three items = 1 REQ, 3 RITMs, each with its own tasks.

// Record producer script
current.setValue('impact', producer.u_impact);
current.setValue('short_description', 'Portal: ' + producer.u_summary);

Same-named variables map automatically; for anything else the producer's script has current (the record being created) and producer (the submitted variables).

With cascade on, values entered for the order guide's own variables are pushed into identically-named variables on each included item — the requester types their details once instead of per item. Off, every item asks again.

A record producer targets one table, so: either three producers behind one catalog page, or one producer targeting the base task table... the cleaner supported answer: keep one producer on a primary table and in its script create the record on the chosen table and abort/redirect — current.setAbortAction(true) after inserting the right GlideRecord, then producer.redirect to the created record. Mention that three separate producers is the maintainable choice.

Because the REQ/RITM structure isn't a single record — it's an orchestrated chain (request, items, variables ownership, fulfillment flow, approvals, cart mechanics) that the catalog engine builds. A producer just inserts one row; pointing it at sc_request would create an empty envelope with no RITM, no variables, no fulfillment. Use the item, that's its job.

Attach the same flow/workflow to all items and branch inside on context: current.cat_item or a variable value drives If/Switch paths. Keep the differences data-driven (a lookup table of per-item settings) rather than hardcoded. In Flow Designer, one flow with a decision step — or a shared subflow with per-item wrapper flows when differences grow.

User criteria: create criteria matching the European locations/departments/groups and add it to the item's "Available for" list (or put everyone-else in "Not available for"). User criteria are the supported visibility mechanism for items, categories and catalogs.

Set the reference variable's default value to javascript:gs.getUserID() — resolved server-side at form load. Client alternative: an onLoad catalog client script with g_form.setValue('requested_for', g_user.userID).

Not directly — producers have no fulfillment engine hook. But the record they create can trigger anything: a flow on "record created" on the target table (conditioned on opened via this producer if needed) gives the same effect. So: "no, but effectively yes, via a trigger on the created record."

Order guides have three steps: describe needs (guide variables) → choose options (the included items, shown per their rule base conditions) → checkout. Items appear on that second step when their rule base conditions evaluate true against the first page's answers — so "page 2" placement = an Item rule whose condition matches.

The condition table deciding which items the guide includes for this requester: each rule pairs a condition (on the guide's initial variables) with items to add. New-hire guide: "needs laptop = yes" → include laptop item; "department = Sales" → include CRM access. It's what makes one guide serve many situations.

On order-guide included items, per-variable cascade/inherit settings control whether the item's variable takes its value from the guide-level variable of the same name. Unchecked, the variable stands alone and the requester fills it on the item. (On other artifacts — UI policies, ACLs — Inherit means "applies to extending tables"; say which context you're answering.)

A reusable audience definition — match by role, group, department, location, company — attached to catalog items/categories, knowledge bases and portal content as "available for / not available for." It's the platform's standard entitlement layer for content: one "EU employees" criteria maintained centrally beats forty per-item conditions.

A producer can target any table the platform lets you create records on — task extensions, custom tables, HR tables — provided the submitting user passes the table's create ACL. Practical cautions rather than hard limits: system tables are a bad idea, and scoped-table access rules apply.

An onSubmit catalog client script: check the variable values, and use a confirm-flag pattern (spModal in portal) — first submit shows the dialog and returns false; confirming sets a flag and resubmits. Same two-phase trick as GlideAjax validation, because you can't block on a dialog asynchronously.

In the producer script after current is set up (or in a flow on the created record): insert the extra task records referencing current.getUniqueValue() as parent. Flow-based is preferred — visible, ordered, and adjustable without editing the producer.