Query ServiceNow's REST APIs from Python by reusing the session you get from logging in through SSO in a browser. No API user, OAuth client, or password.
For the situation where you can read the records perfectly well in the ServiceNow UI but cannot get at them any other way: no integration user, no OAuth client approved, and a service-account request measured in weeks. snowq reads the same records the web UI already shows you, over the same session.
uv sync --extra login
uv run playwright install chromium # skip with --channel chrome / msedge
snowq -i acme login # finish SSO in the browser that opens
snowq -i acme query incident -q "active=true^priority=1" -n 10 -o tableIt is read-only and runs as you, with your own roles and ACLs: it sees exactly what you see in the UI, and it cannot create, update or delete anything. That is the point — it automates the access you already have rather than granting new access. For unattended or shared automation, ask your ServiceNow team for a service account instead; docs/recipes.md says where that line falls. Treat the saved session like a password.
After an SSO (SAML/OIDC) login the instance sets ordinary session cookies
(JSESSIONID, glide_user_route, …). ServiceNow accepts those on its REST
endpoints as long as the request also sends the session's CSRF token, g_ck, in
the X-UserToken header. snowq collects the cookies, picks up g_ck (from the
login capture, the X-UserToken-Response header, or /navpage.do), calls
/api/now/... with both, and saves rotated cookies to
~/.config/snowq/sessions/<host>.session.json for the next run.
When the session expires, snowq exits with code 3 and tells you to log in again.
uv sync --extra login # or --extra all to include browser-cookie import
uv run --extra login playwright install chromiumuv run re-syncs first and drops extras unless you name them, so uv run playwright ... without --extra login uninstalls Playwright and then fails.
Behind Artifactory or another private index, see
docs/install.md — uv ignores pip.conf, which is the usual
reason a working pip and a failing uv sync sit side by side.
error: No solution found when resolving dependencies
cause: Because requests was not found in the package registry ...
That means uv reached no index at all, not that the version pin is wrong —
requests is on every mirror. You do not need uv, a virtualenv, or an install:
requests is the only hard dependency, and corporate Python builds usually
already have it. The ./snowq wrapper in this repo finds a Python that has it,
puts src/ on the path, and runs the CLI straight from the checkout:
./snowq doctor # what is available here
./snowq -i acme import-cookie < cookie.txt
./snowq -i acme whoami
./snowq -i acme query incident -q 'active=true' -n 5 -o tablesnowq.cmd doctor
snowq.cmd -i acme import-cookie < cookie.txtdoctor prints which interpreter it picked, which optional extras are present,
and whether you have a saved session. The wrapper prefers a synced .venv when
there is one, so the same command keeps working after a successful install.
On a blocked index reach for import-cookie, not login — login drives
Playwright, which is the thing that would not install. The wrapper says so up
front instead of failing deep inside the login flow.
docs/sessions.md has the devtools
steps for cookie.txt; docs/install.md covers pointing uv at
your real index.
./snowq is POSIX sh (Git Bash on Windows); snowq.cmd is the same thing for
cmd and PowerShell. Without either, the equivalent is export PYTHONPATH=src
($env:PYTHONPATH = "src" in PowerShell) followed by python -m snowq ... —
skipping that export is what produces No module named snowq.
| Command | When to use it |
|---|---|
snowq -i acme login |
Most reliable. Opens a browser, you finish SSO/MFA, snowq captures the session. |
snowq -i acme import-browser chrome |
Copies cookies from a browser you're already logged into (needs the browser extra). |
snowq -i acme import-cookie |
Fallback: paste the Cookie request header from devtools. Needs no extras. |
-i accepts acme, acme.service-now.com, or a full URL. You can also set
SNOWQ_INSTANCE. Details, and what to do when a session is rejected, are in
docs/sessions.md.
export SNOWQ_INSTANCE=acme
snowq whoami
snowq query incident -q "active=true^priority=1" -f number,short_description,state,assigned_to \
-d true --order-by -sys_created_on -n 20 -o table
snowq query change_request -q "start_date>=javascript:gs.beginningOfToday()" -n 0 -o csv > changes.csv
snowq get incident 9d385017c611228701d22104cc95c371 -d all
snowq count incident -q "active=true"
snowq raw /api/now/table/sys_user_group -p sysparm_limit=5 -p sysparm_fields=name
snowq logout-n 0 fetches every matching record, paging 500 at a time. -d true returns
display values instead of sys_ids for reference and choice fields; -d all returns
both.
-o picks the shape: json (default), jsonl for streaming into jq, csv for
a spreadsheet, and table for reading in the terminal.
A ServiceNow table answers with every field it has — incident is around 150,
most of them empty custom ones — so -f is what makes a table readable:
snowq query incident -q 'active=true' -n 5 -o table \
-f number,short_description,state,assigned_to -d trueWithout -f, table will not print all 150: it drops the fields that are empty
in every row, leads with the ones that identify a record (number, state,
assigned_to, …), keeps what fits one line, and tells you on stderr how many it
held back. csv and json are unfiltered either way, so exports keep every
field. The row count also goes to stderr, so ... -o table 2>/dev/null leaves
just the table.
The -q encoded-query syntax is what the UI list filter builds — build the filter
there, right-click the breadcrumb → Copy query, and paste it. Dot-walking works
too (cmdb_ci.support_group.name=Unix Platform Engineering).
docs/recipes.md has worked examples: incident queues by
assignment group, backlog counts per team, problem management (known errors,
RCA tasks, which problems drive the most incidents), server inventory, CMDB
hygiene, CAB prep, emergency-change audits, and scheduled CSV exports via
examples/export.py.
AGENTS.md is the guidance for LLM-driven use: encoded-query syntax, the flags that decide whether output is usable, exit-code handling, and the rule that matters most — discover field and choice values from the instance rather than guessing them, because a customised instance answers a wrong guess with an empty result rather than an error.
Every assistant looks for its own filename, so the shared core of that file is
generated into each one — Claude Code (CLAUDE.md, plus a skill under
.claude/skills/), Copilot (.github/copilot-instructions.md), Cursor
(.cursor/rules/), Windsurf, Cline and Continue. Edit AGENTS.md, then:
python util/gen_agent_docs.py # regenerate
python util/gen_agent_docs.py --check # CI: fail if one drifteduv run pytest runs that check, because a stale copy still teaches the rule
that changed.
from snowq import SnowClient
from snowq.auth import load_session
stored = load_session("acme")
snow = SnowClient.from_session(stored)
for inc in snow.table("incident", query="active=true^priority=1", fields=["number", "short_description"], limit=None):
print(inc["number"], inc["short_description"])
snow.to_session(stored.source).save() # keep rotated cookiesuv run pytestThe tests use a fake HTTP adapter, so they don't need an instance.