Home/Blog

az ad app list: Filter App Registrations by createdDateTime

Working az ad app list commands to filter, sort and export Entra app registrations by createdDateTime, why you only get 100, and when you need az rest.

az ad app list: Filter App Registrations by createdDateTime

To list Entra ID app registrations by creation date, filter on the server and sort on your machine: az ad app list --filter "createdDateTime ge 2026-01-01T00:00:00Z" --query "sort_by([], &createdDateTime)". Without --all or a filter, the CLI returns only the first 100 apps, so a sort can quietly order the wrong 100. Sorting on the Microsoft Graph side needs advanced query parameters that az ad app list does not send; for that, use az rest or Get-MgApplication.

Key takeaways

  • --filter "createdDateTime ge ..." runs on Microsoft Graph and returns every match. The ge and le operators need no special headers.
  • No filter and no --all means 100 apps and a warning on stderr. Scripts that throw stderr away miss it.
  • sort_by([], &createdDateTime) sorts locally. $orderby=createdDateTime on Graph needs ConsistencyLevel: eventual and $count=true.
  • App registrations have createdDateTime; service principals effectively do not. Join on appId or use the audit log.
  • For TSV, force column order with a multiselect list. For CSV, use jq.

The command that works

The commands below target Azure CLI 2.85.0, released April 7, 2026. The az ad commands have called Microsoft Graph since version 2.37.0, so the Graph rules described here are what decides what works.

Apps registered since the start of the year, oldest first:

az ad app list \
  --filter "createdDateTime ge 2026-01-01T00:00:00Z" \
  --query "sort_by([], &createdDateTime)[].{name:displayName, appId:appId, created:createdDateTime}" \
  --output table

A closed range, counted:

az ad app list \
  --filter "createdDateTime ge 2026-01-01T00:00:00Z and createdDateTime le 2026-03-31T23:59:59Z" \
  --query "length(@)"

Every app registration in the tenant, newest ten first:

az ad app list --all \
  --query "reverse(sort_by([], &createdDateTime))[:10].{name:displayName, appId:appId, created:createdDateTime}" \
  --output table

Three details make these work. The timestamp is UTC with a trailing Z. It has no quotes around it, because Graph treats DateTimeOffset values as literals and a quoted value would be a string. And the identity running the command needs to read applications; a service principal in CI needs the Microsoft Graph Application.Read.All permission.

Why you only see 100 apps

az ad app list with no arguments is built for low latency, not completeness. In the CLI source, the command keeps the first 100 results and, if there were more, logs this warning:

The result is not complete. You can still use '--all' to get all of them with long latency expected, or provide a filter through command arguments

Any filter argument (--filter, --display-name, --app-id, --identifier-uri) or --all returns the full result. Two consequences catch people:

  1. sort_by on the unfiltered command sorts whatever 100 apps Graph happened to return. Your "oldest app" is the oldest of a sample.
  2. The warning goes to stderr. A script that runs 2>/dev/null, or a pipeline that only captures stdout, processes 100 apps and reports success.

--display-name is a prefix match (startswith), not an exact match, which is worth knowing before you use it in a deletion script.

Filtering on the server vs sorting on your machine

Microsoft Graph splits query support for directory objects into what works by default and what needs advanced query parameters: the ConsistencyLevel: eventual header plus $count=true. For the application resource and createdDateTime:

QueryDefaultNeeds advanced parameters
createdDateTime ge or le a timestampYesNo
createdDateTime eq nullNoYes
$orderby=createdDateTimeNoYes
$filter and $orderby in the same requestNoYes
owners/$count eq 0 (apps with no owners)NoYes

The CLI's Graph client sends $filter and nothing else: no custom headers, no $orderby. So filtering belongs in --filter, and sorting belongs in --query, which is JMESPath evaluated locally after all pages arrive. The JMESPath library the CLI ships with also compares strings, which is how the endDateTime filters below work. ISO 8601 timestamps in UTC sort correctly as strings.

When you need az rest and ConsistencyLevel

Use az rest when you want Graph to do the sorting, when you need an advanced filter, or when you want $select to shrink the payload on a large tenant. az rest acquires a Graph token from your CLI login and lets you set headers:

az rest --method get \
  --url 'https://graph.microsoft.com/v1.0/applications' \
  --url-parameters '$filter=createdDateTime ge 2026-01-01T00:00:00Z' \
                   '$orderby=createdDateTime desc' '$count=true' '$top=999' \
                   '$select=displayName,appId,createdDateTime' \
  --headers ConsistencyLevel=eventual \
  --query 'value[].{name:displayName, appId:appId, created:createdDateTime}' \
  --output table

Single quotes keep $filter away from shell expansion in bash and PowerShell alike. az rest returns one page; the application list allows up to 999 per page with $top, and beyond that you follow @odata.nextLink yourself, sending the header on every page:

url='https://graph.microsoft.com/v1.0/applications?$select=displayName,appId,createdDateTime&$orderby=createdDateTime%20desc&$count=true&$top=999'
while [ -n "$url" ]; do
  page=$(az rest --method get --url "$url" --headers ConsistencyLevel=eventual)
  jq -r '.value[] | [.createdDateTime, .appId, .displayName] | @tsv' <<<"$page"
  url=$(jq -r '."@odata.nextLink" // empty' <<<"$page")
done

The Microsoft Graph PowerShell SDK handles paging and headers for you:

Connect-MgGraph -Scopes Application.Read.All
Get-MgApplication -All -ConsistencyLevel eventual -CountVariable appCount `
  -Filter "createdDateTime ge 2026-01-01T00:00:00Z" -Sort "createdDateTime desc" `
  -Property DisplayName,AppId,CreatedDateTime |
  Select-Object CreatedDateTime, AppId, DisplayName

App registrations vs service principals

An app registration (application object) is the definition of an app and lives in its home tenant. A service principal is the app's instance in a tenant where it is used, and it is what holds role assignments and sign-ins. Your own apps have both. Microsoft first-party apps and third-party multitenant apps have only a service principal in your tenant; their registration lives elsewhere.

That is why createdDateTime behaves differently. The v1.0 application resource documents it, with filter and sort support. The v1.0 servicePrincipal resource does not document a createdDateTime property, and Microsoft's own sample output for az ad sp list shows "createdDateTime": null. Do not build reports on it.

For service principals of your own apps, take the date from the registration and join on appId. For third-party service principals, the creation time is in the directory audit log:

az rest --method get \
  --url 'https://graph.microsoft.com/v1.0/auditLogs/directoryAudits' \
  --url-parameters "\$filter=activityDisplayName eq 'Add service principal'" \
  --query 'value[].{when:activityDateTime, app:targetResources[0].displayName, by:initiatedBy.user.userPrincipalName}' \
  --output table

That needs AuditLog.Read.All, and Entra keeps audit logs for seven days on the free tier and 30 days on P1 or P2. Anything older is gone unless you export the logs.

Finding stale and risky apps: owners, secrets, creators

Creation date is usually the first question in an app cleanup, not the last. Three more queries cover the rest.

Client secrets ending in the next 30 days (use keyCredentials for certificates):

az ad app list --all \
  --query "[?passwordCredentials[?endDateTime < '2026-05-09']].{name:displayName, appId:appId, secretEnds:passwordCredentials[].endDateTime}" \
  --output json

Apps with no owners, which nobody will answer for when they break or leak:

az rest --method get \
  --url 'https://graph.microsoft.com/v1.0/applications' \
  --url-parameters '$filter=owners/$count eq 0' '$count=true' '$top=999' \
                   '$select=displayName,appId,createdDateTime' \
  --headers ConsistencyLevel=eventual \
  --query 'value[].{name:displayName, appId:appId, created:createdDateTime}' \
  --output table

Which tools register apps in your tenant, grouped by the createdByAppId Entra records on each registration:

az ad app list --all --query "[].createdByAppId" --output tsv | sort | uniq -c | sort -rn

An unfamiliar creator means some script, pipeline or product is registering apps, and you want to know which before the count grows. Apps that are old, ownerless and holding a live secret go to the top of the list. Right-sizing what those identities can do is the next step; for human admin access, see our guide to just-in-time admin access, and for the wider program, our IAM strategy guide.

Exporting to CSV

jq handles quoting for display names that contain commas:

az ad app list --all --output json |
  jq -r '["displayName","appId","createdDateTime"],
         (sort_by(.createdDateTime)[] | [.displayName, .appId, .createdDateTime]) | @csv' \
  > app-registrations.csv

For TSV, use a multiselect list. The CLI documents that it does not guarantee column order for TSV unless the query selects values as a list:

az ad app list --all \
  --query "sort_by([], &createdDateTime)[].[displayName, appId, createdDateTime]" \
  --output tsv > app-registrations.tsv

What goes wrong in practice

  • The silent 100. A nightly report without --all stays at 100 rows forever, and the warning ends up in a log nobody reads. Fail the job if the row count is exactly 100.
  • Local dates in filters. createdDateTime is UTC. A filter written from a local calendar date shifts the window by your offset.
  • Deleted apps disappear. Soft-deleted registrations do not appear in az ad app list. They sit under directory/deletedItems/microsoft.graph.application in Graph and can be restored for 30 days.
  • Reports built on service principals. Joining sign-in data to service principal creation dates produces empty columns. Use the registration or the audit log.
  • Posture drift. A one-off cleanup decays. Run these queries on a schedule and route results to owners, the same way you would treat any cloud security posture finding.

Frequently Asked Questions

Why does az ad app list only return 100 applications?

Without --all or a filter argument, the Azure CLI keeps only the first 100 applications Microsoft Graph returns and prints a warning that the result is not complete. Add --all to get every app registration, or pass --filter, --display-name, --app-id or --identifier-uri, which return every match. The warning goes to stderr, so scripts that discard stderr never see it.

How do I filter az ad app list by createdDateTime?

Pass an OData filter: az ad app list --filter "createdDateTime ge 2026-01-01T00:00:00Z". The ge and le operators work on createdDateTime without any special headers, and you can combine them with and to get a date range. Write the timestamp in UTC without quotes, because Microsoft Graph expects a DateTimeOffset literal, not a string.

How do I sort app registrations by creation date?

Sort on your machine with JMESPath: add --query "sort_by([], &createdDateTime)" to az ad app list, and wrap it in reverse() for newest first. Use --all or a filter first, otherwise you are sorting an arbitrary 100 apps. Sorting on the Graph side with $orderby needs the ConsistencyLevel: eventual header and $count=true, which az ad app list does not send, so use az rest or Get-MgApplication for that.

Why is createdDateTime empty for service principals?

The Microsoft Graph v1.0 servicePrincipal resource that az ad sp list reads does not document a createdDateTime property, and Microsoft's own sample output for the command shows it as null. For an app registered in your tenant, use the application object's createdDateTime and match on appId. For third-party apps, look for the Add service principal event in the directory audit log while it is still within your retention period.

How do I find app registrations with expiring secrets?

Run az ad app list --all with a JMESPath query on the passwordCredentials array that keeps apps whose endDateTime is earlier than your cutoff date; the full command is in the stale and risky apps section above. Use keyCredentials instead of passwordCredentials for certificates. The CLI only returns credential metadata such as the end date and display name, never the secret value.

How do I export Azure app registrations to CSV?

Run az ad app list --all --output json and convert it with jq using @csv, which quotes display names that contain commas. If tab-separated output is enough, use --output tsv with a multiselect list such as [].[displayName, appId, createdDateTime] in --query, because that is the only way to guarantee the column order.

Need help with Azure identity?

App registrations pile up quietly: secrets nobody rotates, apps nobody owns, permissions nobody remembers granting. We clean that up and put the guardrails in place so it stays clean, and if we built it, we're on the pager for it.