HolySN

Background scripts

Running a script without leaving the page, the panel beside the editor, and why this is the most careful code in the extension.

/bg opens Background Scripts. /bgc opens it preloaded with a gr.get() for the record you are on, and /bgl with a loop over the list you are on, same filter and same page size. /bgp opens the panel beside the editor, and /debug opens the Script Debugger.

The templates are never typed into the page

/bgc and /bgl put the script on your clipboard and in a short-lived stash, then navigate. When the page arrives the stash is written into the editor, retried twenty times at four-tenths of a second apart, and then given up on. Your clipboard still has it.

That looks roundabout until you notice what it avoids. /sys.scripts.do is the most dangerous URL in the product, and a command that navigates there and then pokes at the form is one accident away from submitting it. These commands stay on the harmless side of that line.

The templates use gs.info rather than gs.print. Measured on a live instance with an application scope selected, print is refused outright: Function print is not allowed in scope x_snc_work_app. Use gs.debug() or gs.info() instead, followed by forty lines of stack. gs.info works in global and in every scope.

Running without leaving the page

ServiceNow's own Run Script submits the form, so the browser leaves the editor at once and /sys.scripts.do streams the answer into a bare page. Measured: the old document is gone within a few hundred milliseconds, and on that streaming page no timer ever fires. For the whole run there is nothing on screen — no clock, no cancel, no output — and when it ends you are on a different page from your code.

So the panel makes the same request with the same body while the page stays where it is. Pressing Run Script with the panel open does not navigate: the click and the form submission are both intercepted, and the run happens underneath.

The timeout is ten minutes, because a long script is not a hung one, and cancelling goes through the platform's own cancel-my-transaction page.

Why one run at a time

Every POST to /sys.scripts.do rotates the session's CSRF token. A second POST carrying the old one is treated as forgery and the session is dropped. That was a real "logged out while idle" bug here.

Three things prevent it now. A lock that holds across frames and tabs, so only one run is in flight anywhere. The token is read immediately before posting rather than cached. And the rotated token is read back out of the response and adopted, with a resynchronise for the case where the request died after the server had already rotated it.

The body is copied, not composed

Only the token and the script text are replaced. Everything else goes as the page had it: the scope, "record for rollback", the quota setting, the snippet fields. So in scope and rollback mean exactly what the page in front of you says.

The script is written twice on the modern page, into the editor's mirror and into the hidden original, because guessing which one the platform reads is how you run yesterday's code.

One bug there is worth writing down, because the symptom is so misleading. new FormData(form) leaves out the submit button unless the submitter is passed explicitly. The fixture used to build this was captured from a real button press, where the browser had already added it, so the field was in the measurement, in the test, and never in the actual request. Without it the platform runs nothing and re-renders the page: HTTP 200, no "Script completed", no records, and a run that appears to do absolutely nothing.

The panel

It opens by itself about a second after either background page loads, because the page builds its editor after load and the Run button has to exist first. It has three tabs, and each one reads a table the platform already fills in.

History is sys_script_execution_history: every background script run on the instance, by anyone, with who ran it and when.

Output is this run's log, filterable, with the noise foldable away. A sys_id the script printed becomes a link to its record: the first twenty distinct ones are looked up after the log is drawn, each underlined as its answer arrives, with the table in its title. An id that belongs to no record stays plain text, because an underline that leads nowhere is worse than none.

Records is what the run actually changed, with links. It is not scraped out of the log text: it reads the rollback context and its sequence, which is what the platform itself would use to undo the run. That is why it can name the table and the record rather than a line of text that happened to mention one.

The panel gives the page its width back by padding the body rather than moving anything, and it remembers whatever padding the page already had, so closing hands it back instead of deleting a declaration that was never ours.

The Script Debugger is not a page

There is no URL for it. The platform's own navigator entry runs window.top.launchScriptDebugger(). /$scriptdebugger.do answers 200 with "Page not found", which is the worst way to be broken: the tab opens, and the command looks like it worked.

So /debug looks for that function on the window and on its top, and from a bare classic page it says exactly that rather than opening an empty tab.

What else in the extension runs a script

Auto-Insert's force-save, the promote check, the hygiene scan and its fixes, the scope fallback and the secret reveal all run server-side scripts, through the same lock, with a forty-second timeout, adopting the rotated token the same way.

The frequent paths are deliberately kept off /sys.scripts.do and on read-only REST, for two reasons. It avoids tripping strict CSRF on an idle tab, and every run leaves a Background Script run by entry in the instance's log. A tool that fills your syslog with its own housekeeping is a tool your admin will remove.

Output is parsed back out of the streamed HTML, which prefixes every printed line and escapes it. That is why the extension's own scripts wrap their answers in base64 envelopes rather than printing JSON and hoping.

An empty response says the session may have dropped, and a run that did not run says the script did not run and the page said nothing, because those two are different problems with different fixes.

Background scripts | HolySN