commands

Ten services, one generated tree.

Every service command is generated from Google’s Discovery Service at build time — 308 methods, 839 typed flags, zero hand-written per-service commands. That makes the tree predictable instead of memorized. For the flat, id-based view of the same index, see Discovery API.

308 methods · 10 services · generated, not curated
the naming rule

Predict any command from its method id.

There is nothing to memorize: a Discovery method id, with dots turned into spaces, is the command. The same rule generates every flag.

method id → command
# A method id, with dots turned into spaces, IS the command.
gmail.users.messages.list        grr gmail users messages list
calendar.events.list             grr calendar events list
drive.files.list                 grr drive files list
tasks.tasklists.list             grr tasks tasklists list
sheets.spreadsheets.values.get   grr sheets spreadsheets values get

# Resources nest as subcommands; the method is the leaf. Each leaf also
# carries its bare name as a visible alias, shown in --help.
grr gmail --help                  # "users" and its children, alphabetically
grr gmail users --help            # "messages", "drafts", "labels", ...

# camelCase method names keep their casing as aliases:
grr gmail users getProfile --user-id me

Top level: grr auth, grr api, grr schema, grr transport, plus one subcommand per service. The tree is emitted into src/commands/generated.rs by a generator that reads the committed Discovery index, and a daily workflow opens a PR when Google adds a method — so new surface arrives without a grr release.

Flag names come from the same ids, camelCase collapsed to kebab-case, typed by what Discovery declares:

  • Integers are parsed as numbers: --max-results 10.
  • Booleans are presence flags: --include-spam-trash, not --include-spam-trash true.
  • Repeated parameters repeat: --label-ids INBOX --label-ids UNREAD. (Single-string parameters that take comma lists inside the value, like Drive’s --order-by, are passed through as-is.)
  • Enum parameters validate their values; a bad one is caught before anything is sent.
  • Required parameters are enforced by clap — you cannot build a request missing --user-id.
  • Bodies are untyped, deliberately. Discovery’s request schemas are not part of the distilled index, so POST/PATCH/PUT bodies pass through --params (a JSON object) or --body-file (a file, or - for stdin) verbatim. Typed flags win over --params on conflict.
typed flags · collision rule · bodies
# Flags are generated from Discovery parameter names: camelCase -> kebab-case.
grr gmail users messages list --user-id me \
    --q "is:unread" \
    --max-results 5 \
    --label-ids INBOX --label-ids UNREAD \
    --include-spam-trash

# --max-results is parsed as an integer, --include-spam-trash is a presence
# flag, --label-ids repeats. Enum parameters validate; required ones are
# enforced before anything is sent.

# Collision rule: a parameter literally named "format" or "query" becomes
# --param-format / --param-query (the -f output flag keeps --format).
grr gmail users messages get --user-id me --id <id> --param-format metadata

# Bodies are untyped by design -- Discovery request schemas are not in the
# index -- so they pass through --params or --body-file verbatim.
grr calendar events insert --calendar-id primary --body-file ./event.json

Every leaf also carries the same escape hatches grr api call has: --params, --body-file, repeatable --query KEY=VALUE for raw query pairs, --dry-run, and -f/--format. Dispatch resolves the leaf’s id against the embedded index and funnels into one shared call path — the two routes below produce byte-identical output:

piping · dry runs · parity
# stdout is data, stderr is logs -- so this is always valid JSON.
$ grr gmail users messages list --user-id me --q "from:github.com" --max-results 10 | jq -r '.[0].id'
19f8ab2c

# --dry-run prints the resolved request as JSON and sends nothing.
$ grr gmail users messages list --user-id me --q "is:unread" --dry-run | jq -r '.url'
https://gmail.googleapis.com/gmail/v1/users/me/messages?q=is%3Aunread

# The same method, reached through the flat escape hatch, byte-identical:
$ grr api call gmail.users.messages.list --param userId=me --param 'q=is:unread' --dry-run
the services

Every service, a few real commands.

Verified against the binary and the committed index. Run grr <service> --help for the authoritative version of any branch — these are samples, not the census.

Gmail

79 methods. Messages, threads, drafts, labels, history, attachments, filters, forwarding, POP/IMAP, send-as, CSE, delegates, and watches — the deepest tree.

grr gmail
grr gmail users messages list --user-id me --q "in:inbox" --max-results 5
grr gmail users messages get --user-id me --id <message-id> --param-format metadata
grr gmail users messages attachments get --user-id me --message-id <message-id> --id <attachment-id>
grr gmail users threads list --user-id me --max-results 10
grr gmail users labels list --user-id me
grr gmail users drafts create --user-id me --body-file ./draft.json
grr gmail users settings filters list --user-id me

Calendar

38 methods. Calendars, events, recurring instances, ACL, free/busy, colors, and settings. The calendar id is a parameter like any other — `primary` where one is accepted.

grr calendar
grr calendar events list --calendar-id primary --max-results 10
grr calendar events get --calendar-id primary --event-id <event-id>
grr calendar events insert --calendar-id primary --body-file ./event.json
grr calendar calendarList list
grr calendar freebusy query --body-file ./freebusy.json
grr calendar colors get

Drive

64 methods. Files, permissions, comments, replies, revisions, changes, drives, and approvals — including the long tail no CLI ever wraps by hand.

grr drive
grr drive files list --q "name contains 'report'" --page-size 10
grr drive files get --file-id <file-id>
grr drive files export --file-id <document-id> --mime-type application/pdf
grr drive permissions list --file-id <file-id>
grr drive comments list --file-id <file-id>
grr drive changes list --page-token <token>
grr drive files emptyTrash

People

24 methods. The People API verbatim: resource names look like people/c12345, and the API nests one `people` level, so the command says it twice.

grr people
grr people people connections list --resource-name people/me
grr people people get --resource-name people/c12345
grr people contactGroups list
grr people otherContacts list

Chat

54 methods. Spaces, members, messages, reactions, media, custom emoji, and read state. Space-scoped methods take --parent spaces/… .

grr chat
grr chat spaces list --page-size 10
grr chat spaces messages create --parent spaces/AAAA --body-file ./message.json
grr chat spaces messages list --parent spaces/AAAA --page-size 25
grr chat spaces members list --parent spaces/AAAA

Forms

10 methods. Form bodies, responses, publish settings, and push-notification watches. The API nests one `forms` level, like People does with `people`.

grr forms
grr forms forms get --form-id <form-id>
grr forms forms responses list --form-id <form-id>
grr forms forms watches list --form-id <form-id>

Tasks

14 methods. Task lists and the tasks inside them. Note the scope caveat: this build does not consent to the Tasks scope, so live calls can 403.

grr tasks
grr tasks tasklists list
grr tasks tasks list --tasklist @default
grr tasks tasks insert --tasklist @default --body-file ./task.json

Sheets

17 methods. Spreadsheets, values (read, write, append, batch), and developer metadata. Colon-verb paths like values/{range}:append are handled for you.

grr sheets
grr sheets spreadsheets values get --spreadsheet-id <id> --range Sheet1!A1:B10
grr sheets spreadsheets values update --spreadsheet-id <id> --range Sheet1!A1 --body-file ./values.json
grr sheets spreadsheets values append --spreadsheet-id <id> --range Sheet1!A:A --value-input-option USER_ENTERED
grr sheets spreadsheets get --spreadsheet-id <id>

Docs

3 methods. Documents get, create, and batchUpdate — the smallest service in the index.

grr docs
grr docs documents get --document-id <id>
grr docs documents create --body-file ./new-doc.json
grr docs documents batchUpdate --document-id <id> --body-file ./requests.json

Slides

5 methods. Presentations, pages, and thumbnails. camelCase methods keep their casing as aliases: getThumbnail, not get-thumbnail.

grr slides
grr slides presentations get --presentation-id <id>
grr slides presentations pages getThumbnail --presentation-id <id> --page-object-id g1
grr slides presentations batchUpdate --presentation-id <id> --body-file ./requests.json
output

Four formats, one flag.

Every command takes -f/--format, including the generated leaves. There is no global --format flag: it would collide by id with per-method parameters named format.

json (default), jsonl, table, pretty. JSON is the default because the common consumer is a pipe, not a person.

jsonl emits one JSON document per line for array results, which is what you want when streaming into jq without loading everything. table renders arrays as one row per element with the union of keys as columns, a single object as a Field/Value table, and nested values as compact JSON inside a cell.

output formats
# Every command accepts -f/--format. JSON is the default.
$ grr gmail users messages list --user-id me --max-results 5 -f table
+----------+--------------------+------------------+
| ID       | THREAD_ID          | SNIPPET          |
+----------+--------------------+------------------+
| 19f8ab2c | 19f8ab2c           | Build notes ...  |
+----------+--------------------+------------------+

$ grr gmail users messages list --user-id me -f jsonl
{"id":"19f8ab2c","threadId":"19f8ab2c"}
{"id":"19f8ab31","threadId":"19f8ab31"}

$ grr gmail users messages list --user-id me -f pretty
account level

The four commands that are not a service.

These sit at the top level because they are not about one API. grr schema is the one to build against.

grr auth

login runs the PKCE browser flow, or the device flow with --device. status makes a live profile call through the shared path and reports the account plus the history_id cursor. setup creates and stores your own OAuth client.

grr transport

Prints the negotiated protocol, whether HTTP/3 was requested and effective, whether it fell back, the core count, and io_uring support. Plain text, not JSON — it is a diagnostic, not data.

grr schema

Dumps the entire command tree as JSON: every subcommand, every argument, whether it is required, whether it takes a value, its default, and its possible values. Because the tree itself is generated from the index, schema is a faithful census of all 308 methods.

grr api

list, describe, call, and refresh against the embedded Discovery index — the flat escape hatch that reaches any method by id. Same index, same call path as the tree.

account · transport · schema
$ grr auth status
{"authenticated":true,"email":"you@example.com","messages_total":48213,"threads_total":9120,"history_id":"1280000"}

$ grr transport
negotiated_protocol: HTTP_3
http3_requested: true
http3_effective: true
fell_back: false
...

$ grr schema | jq -r '.subcommands[].name'
["auth","api","transport","schema","calendar","chat","docs","drive","forms","gmail","people","sheets","slides","tasks"]

grr schema is the canonical reference. It is honest about its own size: roughly 290 KB of valid JSON, emitted in well under 100 ms, with zero configuration and before any OAuth exists. Generate a wrapper or an agent tool definition from it — because it is derived from the same tree the binary dispatches on, it cannot drift from the commands that actually exist.

grr schema · the canonical reference
$ grr schema > tree.json        # ~290 KB, valid JSON, well under 100 ms
$ grr schema | jq '.subcommands[] | select(.name == "gmail") | .subcommands[].name'
["users"]
$ grr schema | jq '[.. | objects | select(.name? == "gmail.users.messages.list") | .args[].name]'

Conventions worth knowing

  • Results on stdout, logs on stderr. The tracing subscriber is wired to stderr explicitly, so grr … | jq never receives a log line. The one stdout exception is grr auth login --device, which prints its human-readable URL and code before the final JSON.
  • Paging is yours to drive. The tree does not follow page tokens: --max-results is the API’s own page size, and --page-token takes the nextPageToken from the previous response. That keeps a list command from turning into an unbounded crawl.
  • Booleans are presence flags (--include-spam-trash), a deliberate change from value-taking booleans: a flag is either passed or absent, so a script never has to guess.
  • Errors are actionable. A 403 names the missing scope, an unknown method id returns ranked suggestions, a missing required parameter names itself. Non-zero exit on failure, always.
  • Two commands print plain text, not JSON: grr transport and the device-login line above. --dry-run output is JSON, so it pipes.
  • Timestamps are RFC 3339 (--time-min 2026-10-01T00:00:00Z), or a bare YYYY-MM-DD where the API accepts a date.

Where the tree comes from

Every leaf above is generated from the committed Discovery index at build time and refreshed daily by a workflow PR. The same index drives the flat grr api surface, so the census never splits in two.