ServiceNow’s Table API is how you read and write records programmatically — incidents, change requests, service catalog items, CMDB entries. Here’s how to query it from Ansible using the uri module.

Authentication

ServiceNow uses basic auth for API calls. Store your credentials as variables, not hardcoded values:

1
2
3
4
# group_vars/all.yml or vault
snow_instance: "company.service-now.com"
snow_user: "svc_automation"
snow_password: "{{ vault_snow_password }}"

Test your connection first:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
- name: Test ServiceNow connectivity
  ansible.builtin.uri:
    url: "https://{{ snow_instance }}/api/now/table/incident?sysparm_limit=1"
    method: GET
    user: "{{ snow_user }}"
    password: "{{ snow_password }}"
    force_basic_auth: true
    status_code: 200
    validate_certs: true
  register: snow_test

Basic Table Query

The Table API endpoint is /api/now/table/{table_name}. Common tables:

TableRecords
incidentIncidents
change_requestChange requests
sc_req_itemService catalog request items (RITMs)
cmdb_ci_serverServer CMDB records
sys_userUsers

Query open incidents assigned to a group:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
- name: Get open incidents for PAM team
  ansible.builtin.uri:
    url: "https://{{ snow_instance }}/api/now/table/incident"
    method: GET
    user: "{{ snow_user }}"
    password: "{{ snow_password }}"
    force_basic_auth: true
    headers:
      Accept: application/json
    body_format: json
    status_code: 200
    validate_certs: true
  vars:
    query_params: "sysparm_query=assignment_group.name=PAM Team^state=1&sysparm_fields=number,short_description,sys_id,state&sysparm_limit=50"
  register: incidents_result

Wait — the query parameters need to go in the URL, not the body. Here’s the right way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
- name: Get open incidents for PAM team
  ansible.builtin.uri:
    url: "https://{{ snow_instance }}/api/now/table/incident?sysparm_query=assignment_group.name%3DPAM+Team%5Estate%3D1&sysparm_fields=number,short_description,sys_id,state&sysparm_limit=50"
    method: GET
    user: "{{ snow_user }}"
    password: "{{ snow_password }}"
    force_basic_auth: true
    headers:
      Accept: application/json
    status_code: 200
    validate_certs: true
  register: incidents_result

Filtering with sysparm_query

The sysparm_query parameter uses ServiceNow’s encoded query syntax:

ffffsiiiiteeeealllltdddde=!^==v=fv1avialaelullueudee2O=Rvfaileuled=val2#####enAOsqoNRtutDaatleesquiasls"New"(1=New,2=InProgress,6=Resolved,7=Closed)

Examples:

1
2
3
4
5
6
7
8
# Open RITMs for a specific catalog item
sysparm_query: "cat_item.name=Service Account Management^state=1"

# Records created in the last 7 days
sysparm_query: "sys_created_on>javascript:gs.beginningOfLast7Days()"

# Assigned to specific user
sysparm_query: "assigned_to.user_name=john.smith"

Extracting Results

The API returns results in result as a list:

1
2
3
4
5
6
7
8
9
- name: Process each RITM
  ansible.builtin.debug:
    msg:
      - "RITM: {{ item.number }}"
      - "Requested for: {{ item.requested_for.display_value | default('N/A') }}"
      - "State: {{ item.state }}"
  loop: "{{ incidents_result.json.result }}"
  loop_control:
    label: "{{ item.number }}"

For reference fields (like assigned_to), ServiceNow returns both value (sys_id) and display_value:

1
2
3
4
5
# Get the display value (human readable)
item.assigned_to.display_value  # "John Smith"

# Get the sys_id (for API operations)
item.assigned_to.value  # "a1b2c3d4..."

Getting a Specific Record by sys_id

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
- name: Get specific incident by sys_id
  ansible.builtin.uri:
    url: "https://{{ snow_instance }}/api/now/table/incident/{{ sys_id }}"
    method: GET
    user: "{{ snow_user }}"
    password: "{{ snow_password }}"
    force_basic_auth: true
    headers:
      Accept: application/json
    status_code: 200
    validate_certs: true
  register: incident_detail

- name: Extract fields
  ansible.builtin.set_fact:
    incident_number: "{{ incident_detail.json.result.number }}"
    short_desc: "{{ incident_detail.json.result.short_description }}"
    state: "{{ incident_detail.json.result.state }}"

Handling Pagination

ServiceNow limits results per page (default 10000, but often configured lower). Use sysparm_limit and sysparm_offset:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
- name: Get all records with pagination
  ansible.builtin.uri:
    url: "https://{{ snow_instance }}/api/now/table/sc_req_item?sysparm_query=state=1&sysparm_limit={{ page_size }}&sysparm_offset={{ offset }}"
    method: GET
    user: "{{ snow_user }}"
    password: "{{ snow_password }}"
    force_basic_auth: true
    headers:
      Accept: application/json
    status_code: 200
  vars:
    page_size: 100
    offset: 0
  register: page_result

# Check Link header for next page
- name: Check if more pages exist
  ansible.builtin.set_fact:
    has_next_page: "{{ 'rel=\"next\"' in (page_result.link | default('')) }}"

Updating a Record

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
- name: Update incident state to resolved
  ansible.builtin.uri:
    url: "https://{{ snow_instance }}/api/now/table/incident/{{ sys_id }}"
    method: PATCH
    user: "{{ snow_user }}"
    password: "{{ snow_password }}"
    force_basic_auth: true
    headers:
      Content-Type: application/json
      Accept: application/json
    body_format: json
    body:
      state: "6"
      close_code: "Solution provided"
      close_notes: "Automated by Ansible — service account onboarded successfully."
    status_code: 200
    validate_certs: true
  register: update_result

Common Errors

401 Unauthorized — wrong credentials or the user doesn’t have the rest_api_explorer role.

403 Forbidden — user authenticated but lacks read/write access to the table. Check ACLs in ServiceNow.

400 Bad Request with sysparm_query errors — your query syntax is wrong. Test it in ServiceNow’s filter builder UI first, then URL-encode it for Ansible.

Empty result set — your query is valid but returns nothing. Check sysparm_display_value=true if you need display values instead of sys_ids in reference fields.

1
2
# Add this to see human-readable values in results
url: "...?sysparm_display_value=true&sysparm_query=..."

The Table API covers 90% of ServiceNow automation needs. Once you have authentication working, everything else is just knowing the right table name and field names — which you can always find in ServiceNow under System Definition → Tables.