Output, --query, pagination and errors
Every AWS command answers with JSON shaped exactly like the API response. On call you rarely want all of it: you want one ARN in a variable, a table of who has which key, or the reason a call failed. This lesson is the toolkit for turning responses into answers - the output formats, JMESPath queries, pagination - and for reading the CLI's errors and exit codes, which are more precise than most people realise.
Need to know: --output json|text|table|yaml changes only the rendering. --query is JMESPath (the same language az uses), evaluated by the CLI after it has fetched every page. --output text with a --query is how you feed shell loops. The CLI pages through results for you; --max-items + NextToken + --starting-token page by hand. Errors start with aws: [ERROR]: and exit with 252 (bad command line), 253 (configuration or credentials), 254 (the service said no) or 255 (anything else).
Four ways to print the same answer
$ aws iam list-users --query 'Users[?starts_with(UserName, `svc-`)]'
[
{
"Path": "/",
"UserName": "svc-orders",
"UserId": "AIDASWXVBJWYSQHFHCEBG",
"Arn": "arn:aws:iam::111122223333:user/svc-orders",
"CreateDate": "2026-02-14T20:00:03+00:00"
},
{
"Path": "/",
"UserName": "svc-payments",
"UserId": "AIDALZHYHVCXLVHKUGWVM",
"Arn": "arn:aws:iam::111122223333:user/svc-payments",
"CreateDate": "2026-03-26T20:00:03+00:00"
},
{
"Path": "/",
"UserName": "svc-search",
"UserId": "AIDAJWNQRKXQVDUSPZPZP",
"Arn": "arn:aws:iam::111122223333:user/svc-search",
"CreateDate": "2026-06-19T20:00:03+00:00"
}
]
$ aws iam list-users --query 'Users[].[UserName,CreateDate]' --output text
ana 2026-08-23T20:00:03+00:00
ci-deploy 2026-07-24T20:00:03+00:00
learner 2026-08-23T20:00:03+00:00
mihai 2026-09-10T20:00:03+00:00
svc-orders 2026-02-14T20:00:03+00:00
svc-payments 2026-03-26T20:00:03+00:00
svc-search 2026-06-19T20:00:03+00:00
$ aws iam list-users --query 'Users[].{User:UserName,Created:CreateDate}' --output table
-----------------------------------------------
| ListUsers |
+----------------------------+----------------+
| Created | User |
+----------------------------+----------------+
| 2026-08-23T20:00:03+00:00 | ana |
| 2026-07-24T20:00:03+00:00 | ci-deploy |
| 2026-08-23T20:00:03+00:00 | learner |
| 2026-09-10T20:00:03+00:00 | mihai |
| 2026-02-14T20:00:03+00:00 | svc-orders |
| 2026-03-26T20:00:03+00:00 | svc-payments |
| 2026-06-19T20:00:03+00:00 | svc-search |
+----------------------------+----------------+
$ aws sts get-caller-identity --output yaml
UserId: AIDAEVXTZTVCSMBGJ2WDW
Account: '111122223333'
Arn: arn:aws:iam::111122223333:user/learner
- json (the default): the response, members in the API's own order, 4-space indent.
- text: tab-separated values, one row per list element - the format for
cut,while readand$(...). Keys are dropped, so always pick the fields with--query. - table: for humans; the title is the API operation name (
ListUsers). - yaml / yaml-stream: handy for policy documents.
--output offprints nothing (the exit code still tells you whether it worked).
The default comes from the profile's output setting or AWS_DEFAULT_OUTPUT.
--query: JMESPath on the client
The expression runs on the JSON response, before it is printed:
| Expression | Means |
|---|---|
Users[].UserName | the UserName of every element (a projection) |
Users[0] / Users[-1] | the first / last element |
Users[?UserName=='ana'] | a filter (string literals in single quotes) |
`Users[?starts_with(UserName, svc-)]` | a function in a filter (backticks = a JSON literal) |
Users[].[UserName,Arn] | several fields as a list per element |
Users[].{Name:UserName,Id:UserId} | renamed fields (table and json) |
length(Users) | count |
sort_by(Users, &CreateDate)[-1].UserName | the newest user |
Contents[?Size > 50000].Key | numbers as backtick literals |
Quote the whole expression in single quotes so bash leaves the brackets, ? and * alone.
$ aws iam list-users --query 'length(Users)'
7
$ aws iam list-users --query 'sort_by(Users, &CreateDate)[-1].UserName' --output text
mihai
$ for u in $(aws iam list-users --query 'Users[].UserName' --output text); do echo "$u: $(aws iam list-access-keys --user-name $u --query 'length(AccessKeyMetadata)')"; done
ana: 0
ci-deploy: 1
learner: 1
mihai: 0
svc-orders: 1
svc-payments: 1
svc-search: 1
--query filters after the data arrived. Many APIs can filter on the server too, which is faster on big accounts and the only way on huge ones: IAM's --path-prefix, S3's --prefix and --delimiter, EC2's --filters. Use the server filter to fetch less, --query to shape what came back.
$ aws s3api list-objects-v2 --bucket oncall-lab-logs-111122223333 --prefix nginx/2026/10/08/ --query 'Contents[].[Key,Size]' --output text
nginx/2026/10/08/access-00.log.gz 81352
nginx/2026/10/08/access-04.log.gz 96081
nginx/2026/10/08/access-08.log.gz 20810
nginx/2026/10/08/access-12.log.gz 35539
nginx/2026/10/08/access-16.log.gz 50268
$ aws s3api list-objects-v2 --bucket oncall-lab-logs-111122223333 --prefix nginx/2026/10/ --delimiter / --query 'CommonPrefixes[].Prefix'
[
"nginx/2026/10/01/",
"nginx/2026/10/02/",
"nginx/2026/10/03/",
"nginx/2026/10/04/",
"nginx/2026/10/05/",
"nginx/2026/10/06/",
"nginx/2026/10/07/",
"nginx/2026/10/08/"
]
Pagination
AWS APIs return results in pages (100 users per call for IAM, 1,000 keys for S3) with a marker for the next page. The CLI follows the markers for you and merges the pages, so aws iam list-users returns everyone. Three options change that:
--max-items N: print at most N items, plus aNextToken; pass it back with--starting-tokento continue;--page-size N: the size of each underlying API call - the CLI still fetches everything, in smaller calls (the cure for calls that time out on huge buckets);--no-paginate: only the first API page, with the service's own marker (IsTruncated,Marker,NextContinuationToken).
$ aws s3api list-objects-v2 --bucket oncall-lab-logs-111122223333 --max-items 3 --query '{Keys: Contents[].Key, Next: NextToken}'
{
"Keys": [
"nginx/2026/10/01/access-00.log.gz",
"nginx/2026/10/01/access-04.log.gz",
"nginx/2026/10/01/access-08.log.gz"
],
"Next": "eyJDb250aW51YXRpb25Ub2tlbiI6IG51bGwsICJib3RvX3RydW5jYXRlX2Ftb3VudCI6IDN9"
}
$ aws s3api list-objects-v2 --bucket oncall-lab-logs-111122223333 --max-items 3 --starting-token eyJDb250aW51YXRpb25Ub2tlbiI6IG51bGwsICJib3RvX3RydW5jYXRlX2Ftb3VudCI6IDN9 --query 'Contents[].Key'
[
"nginx/2026/10/01/access-12.log.gz",
"nginx/2026/10/01/access-16.log.gz",
"nginx/2026/10/02/access-00.log.gz"
]
The token is opaque - here base64 of a small JSON with the position - and belongs to the CLI, not to the service. Note that --query runs on what --max-items kept: length(...) with --max-items 3 says 3.
Reading errors
A failed command prints one line that names the error code, the API operation and the service's message (the CLI's format since 2.34):
$ aws iam get-user --user-name ghost
aws: [ERROR]: An error occurred (NoSuchEntity) when calling the GetUser operation: The user with name ghost cannot be found.
$ echo $?
254
$ aws iam create-user
aws: [ERROR]: the following arguments are required: --user-name
usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:
aws help
aws <command> help
aws <command> <subcommand> help
$ echo $?
252
$ AWS_SHARED_CREDENTIALS_FILE=/dev/null aws sts get-caller-identity
aws: [ERROR]: Unable to locate credentials. You can configure credentials by running "aws configure".
$ echo $?
253
$ aws iam list-users --max-itemz 3
usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:
aws help
aws <command> help
aws <command> <subcommand> help
Unknown options: --max-itemz, 3
$ echo $?
252
The return code tells a script what kind of failure it was:
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | an aws s3 transfer failed (one or more files) |
| 2 | an aws s3 command skipped files it could not read |
| 130 | interrupted (Ctrl+C) |
| 252 | the command line is wrong: unknown option, missing argument, bad JSON |
| 253 | the configuration is wrong or incomplete: no credentials, no Region, unknown profile |
| 254 | the request was sent and the service returned an error (AccessDenied, NoSuchEntity...) |
| 255 | anything else (connection errors, a failed assume-role, a bad --query) |
So if aws iam get-user --user-name "$u" >/dev/null 2>&1 is not "the user exists": 254 can also be AccessDenied. Read the code - or better, the message. The error format is configurable (--cli-error-format, or cli_error_format in the profile):
$ aws iam get-user --user-name ghost --cli-error-format json
{
"Code": "NoSuchEntity",
"Message": "The user with name ghost cannot be found."
}
$ aws iam get-user --user-name ghost --cli-error-format legacy
An error occurred (NoSuchEntity) when calling the GetUser operation: The user with name ghost cannot be found.
legacy is the format before 2.34 (no aws: [ERROR]: prefix): what most blog posts and old runbooks show.
--debug: what the CLI actually did
--debug logs every step to stderr: the version, the arguments, the credential chain, the request and the response headers. The two lines you usually want:
$ aws sts get-caller-identity --debug 2>&1 | grep -E 'credentials|Making request' | cut -c 1-150
2026-09-22 20:00:06,106 - MainThread - botocore.credentials - DEBUG - Looking for credentials via: env
2026-09-22 20:00:06,109 - MainThread - botocore.credentials - DEBUG - Looking for credentials via: assume-role
2026-09-22 20:00:06,112 - MainThread - botocore.credentials - DEBUG - Looking for credentials via: assume-role-with-web-identity
2026-09-22 20:00:06,115 - MainThread - botocore.credentials - DEBUG - Looking for credentials via: sso
2026-09-22 20:00:06,118 - MainThread - botocore.credentials - DEBUG - Looking for credentials via: shared-credentials-file
2026-09-22 20:00:06,121 - MainThread - botocore.credentials - INFO - Found credentials in shared credentials file: ~/.aws/credentials
2026-09-22 20:00:06,124 - MainThread - botocore.endpoint - DEBUG - Making request for OperationModel(name=GetCallerIdentity) with params: {'url_path':
Scripts: the pager, skeletons, input files
- The pager. On a terminal, CLI v2 sends long output through
less. In scripts, cron and CI setAWS_PAGER=""(orcli_pager =in the profile, or--no-cli-pager), or a job can hang waiting forq. - Skeletons.
--generate-cli-skeletonprints every parameter of an operation as JSON; fill it in and pass it back with--cli-input-json file://params.json- handy for operations with big nested parameters. - Files as parameters. Any parameter value can come from a file:
file://policy.json(text) orfileb://blob.bin(binary). Policies are almost always passed this way.
$ aws iam create-user --generate-cli-skeleton
{
"UserName": "",
"Path": "",
"PermissionsBoundary": "",
"Tags": [
{}
]
}
In an interview: "How do you get one value out of the AWS CLI into a shell variable?" - "With
--queryto select the field and--output textto print it bare:ARN=$(aws iam get-role --role-name app --query Role.Arn --output text). The query runs on the client, after the CLI fetched every page."
You can now: pick an output format for a human or a script, write JMESPath queries with projections, filters and functions, page through results by hand, tell a 252 from a 253 from a 254, and use --debug to see which credentials and endpoint the CLI used.