Skill v1.0.1
currentAutomated scan100/100+2 new
version: "1.0.1" name: snu-form-automation description: Drive live ServiceNow forms via the g_form bridge (navigate, set_field, get_form_state, run_ui_action, click_element): insert vs update verbs, optimistic-write verification, auto-handled native dialogs, and auto-filling mandatory reference fields. Read this when automating or visually verifying a form/UI page/widget.
<!-- SN-SCRIPTSYNC:SKILL apiVersion=17 -->
SN ScriptSync — Live Form Automation
How to control and verify live ServiceNow forms through the authenticated browser session.
🤖 Live Form Automation & Visual Verification
📸 Verify visually EARLY — don't loop blind
- When building or automating anything visual (UI Pages, forms, widgets), or when a call
doesn't behave as expected, take a screenshot to confirm the real page state instead of iterating blindly through more API calls.
- A single screenshot often reveals the actual blocker that would otherwise cost many
speculative attempts — a mandatory-field validation banner, a 404 "page not found", an unsaved-changes state, or simply the wrong browser tab.
- **Rule of thumb: if two consecutive attempts don't do what you expect, STOP and
screenshot + read the page before a third attempt.**
Save verbs: insert (new) vs update (existing)
Use the correct UI action for the record's state — the wrong one silently no-ops:
| Record state | run_ui_action verb | Form button | |
|---|---|---|---|
New (sys_id=-1) | sysverb_insert | "Submit" | |
| Existing | sysverb_update | "Update" / "Save" |
Calling sysverb_update on a new record returns { "triggered": true } but inserts nothing.
Live-form commands are optimistic — always verify server-side
set_field, run_ui_action, and click_element report what was sent / triggered / clicked in the browser, not what committed. triggered: true means the UI action fired, NOT that the save succeeded — a mandatory field or client/UI-policy validation can still block it. Never report success on `triggered: true` alone.
Confirm the write before reporting success:
query_recordsby a distinguishing field (number=…,short_descriptionLIKE…) —
required after a new-record insert, or
get_recordby sys_id, or- a
take_screenshot(URL/title flipping fromsys_id=-1to a real number, plus a populated
activity stream, is proof of an insert).
Two traps that look like success but aren't:
- **
get_form_statereturns a pre-allocatedsys_idandnumbereven on an UNSAVED new
record** (ServiceNow assigns these client-side at form load). The form having a sys_id/number is NOT proof the record persisted.
- `get_form_state.mandatory` reflects DICTIONARY-level mandatory only. UI-policy-enforced
mandatory fields (shown yellow on the form) are not flagged — screenshot before saving to catch them.
Native dialogs are auto-handled (no hang)
Browser-native confirm() / alert() / prompt() and the dirty-form "Leave site?" (beforeunload) prompt would otherwise freeze the tab and hang the bridge with no user to click them. The live-form commands neutralize these automatically:
run_ui_action/click_element—confirm()is auto-accepted,alert()/prompt()
swallowed (suppressDialogs: true by default).
navigate— the dirty-form guard is dropped so the page changes without stalling
(discardUnsaved: true by default).
Two consequences to be deliberate about:
run_ui_action sysverb_deleteauto-confirms the "Are you sure?" prompt — **the record is
deleted. To prevent surprise deletes this verb is gated**: it returns E_DISABLED unless sn-scriptsync.deleteRecords.enabled is on. Prefer delete_record (REST) for removals; it never raises a dialog.
navigateaway from a form with unsaved edits discards those edits. Save first with
run_ui_action if you need to keep them, or pass discardUnsaved: false.
Recommended new-record cycle
navigate (table.do?sys_id=-1)→ set_field (all mandatory + content fields)→ take_screenshot (confirm filled; catch yellow UI-policy-mandatory fields)→ run_ui_action sysverb_insert→ query_records (verify the record actually exists) ← REQUIRED→ report result
For an existing record the flow is the same but with sysverb_update and a get_record / get_form_state verification. Prefer set_field (fires client scripts / UI policies) over a raw REST update_record when you need the form's client logic to run. If a save didn't persist, screenshot to read the validation banner, fill the missing fields, and retry once.
Auto-fill mandatory REFERENCE fields (no hardcoded sys_ids)
get_table_metadata(formTable)→ find columns withtype=referenceand their reference
table (this resolves table inheritance for you).
- For each mandatory reference column:
get_parent_options(referenceTable)→ take an
option.sys_id (prefer active=true; mind reference qualifiers).
set_field(column, sys_id).
- Raw fallback:
query_records('sys_dictionary', 'nameIN<table>,<parents>^element=<field>^referenceISNOTEMPTY')
→ reference table, then query_records(refTable, …, limit 1).
- Inheritance gotcha: fields defined on a parent table (e.g.
task) won't appear under
name=<child> — query across the whole table hierarchy.
Commands
navigate ⚡ (Remote - Async)
Navigate a connected ServiceNow tab to a URL (opening a tab if none is found) and resolve once the page finishes loading. Use this to put a specific record/list in front of the live-form commands (set_field, get_form_state, run_ui_action). For a screenshot in the same step, prefer navigate_and_screenshot.
Request:
{"id": "nav_1","command": "navigate","params": { "url": "https://dev12345.service-now.com/incident.do?sys_id=-1" }}
Parameters:
url(required): The http(s) URL to open. (sys_id=-1opens a new record form.)tabId(optional): Navigate this specific tab instead of finding one by pattern.newTab(optional, defaultfalse): Open the URL in a new tab instead of reusing an existing ServiceNow tab.waitForLoad(optional, defaulttrue): Resolve only after the tab reports loadcomplete(capped at 30s).discardUnsaved(optional, defaulttrue): Drop a dirty-form "Leave site?" guard before navigating, so unsaved changes don't stall the navigation on a prompt. ⚠️ Any unsaved edits on the current form are discarded. Setfalseto keep the guard (the navigation may then time out if the form is dirty); save first withrun_ui_actionif you need to keep the changes.
Response (success):
{"id": "nav_1","command": "navigate","status": "success","timestamp": 1733779200000,"result": {"navigated": true,"tabId": 12345,"url": "https://dev12345.service-now.com/incident.do?sys_id=-1","title": "Incident | ServiceNow"}}
Response (error):
{"id": "nav_1","command": "navigate","status": "error","code": "E_INVALID_PARAMS","error": "Only http(s) URLs are allowed"}
Error codes: E_INVALID_PARAMS (missing/invalid URL), E_NO_TAB (could not activate/open a tab), E_BROWSER_DISCONNECTED, E_TIMEOUT.
set_field ⚡ (Remote - Async)
Set a field value on the active ServiceNow form in the connected browser tab via g_form.setValue. Because this drives the live form, it fires client scripts, onChange handlers, and UI policies — unlike a REST update_record, which bypasses all client-side logic. Use it to reproduce/verify client behaviour, not for bulk data writes.
Request:
{"id": "sf_1","command": "set_field","params": {"field": "short_description","value": "Network down in building C"}}
Parameters:
field(required): Field (column) name, e.g.short_description,assigned_to.value(required): Value to set. For reference fields pass the sys_id; adddisplayValueto set the shown label too.displayValue(optional): Display value for reference/choice fields (g_form.setValue(field, value, displayValue)).url(optional): URL pattern to find the tab (default:https://*.service-now.com/*).tabId(optional): Specific browser tab ID to target.
Response (success):
{"id": "sf_1","command": "set_field","status": "success","timestamp": 1733779200000,"result": {"set": true,"field": "short_description","value": "Network down in building C","displayValue": "Network down in building C"}}
Response (error):
{"id": "sf_1","command": "set_field","status": "error","code": "E_NO_FORM","error": "No active form on this page"}
Error codes: E_INVALID_PARAMS (missing field/value), E_NO_FORM (no g_form on the page), E_NOT_FOUND (field not on the form), E_BROWSER_DISCONNECTED, E_TIMEOUT.
Tip: Run get_form_state after set_field to confirm the value (and any dependent fields recalculated by client logic) before triggering a save.
get_form_state ⚡ (Remote - Async)
Read the live form in the connected browser tab: its table, sys_id, new-record flag, and current field values (including unsaved edits). Reads from g_form, so it reflects what the user/agent actually sees — not the saved database row. Pair it with set_field to verify writes and with /tn (run_slash_command) when you need technical field names.
Request (all fields):
{"id": "gfs_1","command": "get_form_state"}
Request (subset):
{"id": "gfs_2","command": "get_form_state","params": { "fields": ["state", "assigned_to", "short_description"] }}
Parameters:
fields(optional): Array of field names to read. Omit to read all fields on the form.url(optional): URL pattern to find the tab (default:https://*.service-now.com/*).tabId(optional): Specific browser tab ID to target.
Response (success):
{"id": "gfs_2","command": "get_form_state","status": "success","timestamp": 1733779200000,"result": {"table": "incident","sys_id": "a1b2c3...","isNewRecord": false,"fields": {"state": { "value": "2", "displayValue": "In Progress", "mandatory": false, "readOnly": false },"assigned_to": { "value": "62826bf0...", "displayValue": "Beth Anglin" },"short_description": { "value": "Network down", "displayValue": "Network down" }}}}
Response (error):
{"id": "gfs_1","command": "get_form_state","status": "error","code": "E_NO_FORM","error": "No active form on this page"}
Error codes: E_NO_FORM (no g_form on the page), E_BROWSER_DISCONNECTED, E_TIMEOUT.
run_ui_action ⚡ (Remote - Async)
Trigger a UI action on the active ServiceNow form in the connected browser tab. Runs the real client-side path (g_form.save() / g_form.submit()), so onSubmit client scripts and UI policies execute. A page reload usually follows, so the command resolves as soon as the action is dispatched (not when the reload finishes) — re-read with get_form_state afterward.
Request:
{"id": "rua_1","command": "run_ui_action","params": { "uiAction": "save" }}
Parameters:
uiAction(optional, defaultsave): One of:save—g_form.save()(save and stay on the form)submit—g_form.submit()(submit the default action)- a named UI action —
g_form.submit("<sysverb>"), e.g.sysverb_update,sysverb_insert,sysverb_delete, or a custom action name
⚠️ Pick the verb that matches the record state: use sysverb_insert ("Submit") on a new record (sys_id=-1) and sysverb_update ("Update"/"Save") on an existing one. Calling sysverb_update on a new record returns { "triggered": true } but inserts nothing.
🔒 Destructive verbs are gated: any delete verb (sysverb_delete, or a custom action whose name contains delete) is rejected with E_DISABLED unless sn-scriptsync.deleteRecords.enabled is on — the same guard that protects delete_record. Prefer delete_record for removals; it never raises a dialog.
suppressDialogs(optional, defaulttrue): Auto-handle native browser dialogs the action may raise —confirm()is auto-accepted,alert()/prompt()are swallowed — so the tab doesn't freeze on a modal no user will answer. ⚠️ This means `sysverb_delete`'s "Are you sure?" confirmation is accepted automatically and the record is deleted. Setfalseonly if you want the native dialog to appear (rarely useful headless).url(optional): URL pattern to find the tab (default:https://*.service-now.com/*).tabId(optional): Specific browser tab ID to target.
Response (success):
{"id": "rua_1","command": "run_ui_action","status": "success","timestamp": 1733779200000,"result": { "triggered": true, "uiAction": "save" }}
Response (error):
{"id": "rua_1","command": "run_ui_action","status": "error","code": "E_NO_FORM","error": "No active form on this page"}
Error codes: E_NO_FORM (no g_form on the page), E_DISABLED (a delete verb while deleteRecords.enabled is off), E_BROWSER_DISCONNECTED, E_TIMEOUT.
Note: success confirms the action was dispatched, not that the save succeeded server-side. If a mandatory field or an onSubmit script blocks the save, the form stays open — verify with a follow-up get_form_state or a take_screenshot.
click_element ⚡ (Remote - Async)
Click a DOM element by CSS selector in the ServiceNow content document of the connected tab. Best-effort and light-DOM only — it does not pierce shadow DOM (Polaris/Next Experience web components). For form fields and UI actions prefer set_field / run_ui_action; reach for click_element only when there is no g_form path (e.g. a classic UI button or link).
Request:
{"id": "ce_1","command": "click_element","params": { "selector": "#sysverb_update" }}
Parameters:
selector(required): A CSS selector resolved against the content document (thegsft_mainiframe in classic UI, otherwise the top document).suppressDialogs(optional, defaulttrue): Auto-handle native dialogs the click may raise (confirm()auto-accepted,alert()/prompt()swallowed) so the tab doesn't freeze on a modal. Setfalseto let a native dialog appear.url(optional): URL pattern to find the tab (default:https://*.service-now.com/*).tabId(optional): Specific browser tab ID to target.
Response (success):
{"id": "ce_1","command": "click_element","status": "success","timestamp": 1733779200000,"result": { "clicked": true, "selector": "#sysverb_update" }}
Response (error):
{"id": "ce_1","command": "click_element","status": "error","code": "E_NOT_FOUND","error": "Element not found: #sysverb_update"}
Error codes: E_INVALID_PARAMS (missing/invalid selector), E_NOT_FOUND (no element matches), E_BROWSER_DISCONNECTED, E_TIMEOUT.