2025, Dec 21 23:00
Azure Resource Graph pagination: skip_token is always None? Add a stable order by for true paging
Learn why Azure Resource Graph returns skip_token=None and result_truncated=true, and how a stable order by enables reliable pagination. Includes code.
Azure Resource Graph pagination can be deceptively tricky: you run a query, get a large result set, but the skip_token is always empty. Without a skip_token, you can’t page through results using the server-provided continuation token, which makes it impossible to fetch everything reliably.
The issue: skip_token is always None
The following code builds a Resource Graph query and tries to iterate through pages. The logic is straightforward, but skip_token never appears in the response.
from azure.identity import DefaultAzureCredential
from azure.mgmt.resourcegraph import ResourceGraphClient
from azure.mgmt.resourcegraph.models import QueryRequest, QueryRequestOptions
auth_ctx = DefaultAzureCredential()
rg_client = ResourceGraphClient(auth_ctx)
kql_stmt = """
Resources
| where type == "microsoft.network/networkinterfaces"
| where isnotnull(managedBy)
| mv-expand ipConfig = properties.ipConfigurations
| where isnotnull(ipConfig.properties.privateLinkConnectionProperties.fqdns)
| project fqdns = ipConfig.properties.privateLinkConnectionProperties.fqdns
"""
collected_fqdns = []
page_marker = None
while True:
req_payload = QueryRequest(
query=kql_stmt,
options=QueryRequestOptions(
skip_token=page_marker,
top=1000,
),
)
resp = rg_client.resources(req_payload)
page_marker = resp.skip_token # remains None
A typical response in this situation looks like this:
{'additional_properties': {}, 'total_records': 10070, 'count': 1000, 'result_truncated': 'true', 'skip_token': None, 'data': [ ... ], 'facets': [] }
What actually happens
skip_token is only populated when you sort results. To enable proper pagination and receive a valid skip_token, add a stable order by clause to your query.
To enable proper pagination and receive a valid skip_token, modify your query to include a stable order by clause.
Another detail that affects expectations: result_truncated is true only if you do not receive a skip_token. That behavior is documented, but it is counterintuitive because you still can’t paginate without a token, and blindly skipping records makes sense only when the order is deterministic.
The official documentation does note related exclusions for when $skipToken won’t be present:
The response won't include the $skipToken if: The query contains a limit or sample/take operator. All output columns are either dynamic or null type.
Reference: https://learn.microsoft.com/en-us/azure/governance/resource-graph/concepts/work-with-data#paging-results
The fix: add a stable order by
Introducing a deterministic sort makes Resource Graph return a valid skip_token. The following code uses order by and iterates through all pages until the token disappears.
from azure.identity import DefaultAzureCredential
from azure.mgmt.resourcegraph import ResourceGraphClient
from azure.mgmt.resourcegraph.models import QueryRequest, QueryRequestOptions
signer = DefaultAzureCredential()
graph = ResourceGraphClient(signer)
kql_query = """
Resources
| where type == "microsoft.network/networkinterfaces"
| where isnotnull(managedBy) and not(managedBy == "")
| where location == "eastus2"
| mv-expand ipConfig = properties.ipConfigurations
| where isnotnull(ipConfig.properties.privateLinkConnectionProperties.fqdns) and array_length(ipConfig.properties.privateLinkConnectionProperties.fqdns) > 0
| project name, fqdns = ipConfig.properties.privateLinkConnectionProperties.fqdns
| order by name asc
"""
fqdn_bucket = []
continuation = None
while True:
qry = QueryRequest(
query=kql_query,
options=QueryRequestOptions(
skip_token=continuation
),
)
page = graph.resources(qry)
for row in page.data:
pass # process rows
if page.skip_token is None:
break
else:
continuation = page.skip_token
Why this matters
When you query at scale, you need predictable pagination to collect complete datasets. A stable order by is essential because without a consistent sort order the backend does not return skip_token, and attempting to replicate pagination by manually skipping rows is unreliable. Deterministic sorting ensures consistent pages and unlocks server-driven continuation.
Takeaways
If skip_token is empty, make the result order deterministic with an order by clause. Avoid operators that suppress $skipToken as documented, and remember that result_truncated being true without a token doesn’t mean you can page. The most reliable path to complete enumeration with Azure Resource Graph is to add a stable sort and then follow skip_token until it disappears.