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.