discovery

The index behind the whole tree.

Every service command grr runs is generated from a machine-readable description of Google’s APIs, distilled into an index and compiled into the binary. grr api is the same index read flat, by method id.

402 methods · 14 APIs · works offline

Why this exists

A hand-written command tree can never keep up with a Google Workspace API. There are 402 methods across 14 services, and Google adds more without asking.

So grr does not hand-write one. The committed Discovery index is the single source of truth for the entire surface: a build-time generator compiles it into the namespaced command tree (grr gmail users messages list — see Commands), and grr api reads the same index flat by method id. A method released after this build of grr existed reaches the tree through the daily refresh PR — no code change, no new release, no upgrade step.

Two properties make this more than a thin passthrough:

  • It works offline. The index is compiled in, not fetched at runtime. grr api list and grr api describe need no network, no cache directory, and no API key, so an agent can discover what is available before it has a credential.
  • Scope is reported per method, consented per login. Each method carries its own scope list, and grr api describe reports the narrowest entry. Consent is not per method: grr auth login asks for a small default set and --service <name> widens it from the same index, so a call is never gated on a narrower sibling the build did not ask for — see the caveat below.

The distilled index is about 2.6 MiB across 14 service documents plus the manifest, against several megabytes for the raw documents — a meaningful difference in a binary whose size is a selling point.

browse

list, describe, refresh.

Two read-only commands that need neither a network connection nor a login — and one that pulls the index forward.

grr api list prints an array of { id, http, summary }. The count goes to stderr and the array to stdout, so grr api list | jq stays valid JSON. --service narrows to one service, --filter is a case-insensitive substring match over both method ids and descriptions, and --grouped nests the methods under their service with a per-service count.

grr api list
$ grr api list
402 methods                                  # to stderr
[{"id":"calendar.acl.delete","http":"DELETE","summary":"Deletes an access control rule."}, ...]

$ grr api list --service sheets
$ grr api list --filter list --grouped
$ grr api list --service gmail --filter "batch"

grr api describe takes one method id and returns everything needed to call it: the HTTP verb, the raw path template, the full scope list, the narrowest scope, and every parameter with its type, required flag, repetition, and enum values. The hint field is a ready-made invocation using the method’s first parameter.

grr api describe
$ grr api describe gmail.users.messages.list
{
  "id": "gmail.users.messages.list",
  "service": "gmail",
  "apiVersion": "v1",
  "discoveryRevision": "20260928",
  "httpMethod": "GET",
  "path": "gmail/v1/users/{userId}/messages",
  "urlTemplate": "https://gmail.googleapis.com/gmail/v1/users/{userId}/messages",
  "description": "Lists the messages in the user's mailbox.",
  "scopes": ["https://mail.google.com/", "..."],
  "leastPrivilegeScope": "https://www.googleapis.com/auth/gmail.readonly",
  "parameters": [
    {"name":"userId","type":"string","required":true,"repeated":false,...},
    {"name":"q","type":"string","required":false,"repeated":false,...},
    {"name":"maxResults","type":"integer","required":false,...}
  ],
  "hint": "grr api call gmail.users.messages.list --params '{\"userId\": "..."}'"
}

grr api refresh [--service X] fetches fresh Discovery documents into the local cache between releases, for the rare method you need before the next daily PR lands. The embedded index keeps working offline regardless.

coverage

402 methods, 14 services.

Counts are what grr api list reports at runtime. Revisions come from the committed index at src/discovery/manifest.json.

Gmail

Messages, threads, drafts, labels, history, filters, forwarding, send-as, and settings.

79 methods · v1 · revision 20261106

grr gmail

Calendar

Calendars, events, instances, ACL, free/busy, colors, and settings.

38 methods · v3 · revision 20261109

grr calendar

Drive

Files, permissions, comments, revisions, changes, drives, and team drives.

64 methods · v3 · revision 20261109

grr drive

People (Contacts)

Contacts, connections, other contacts, contact groups, and directory people.

24 methods · v1 · revision 20261111

grr people

Chat

Spaces, messages, memberships, reactions, attachments, sections, and availability.

55 methods · v1 · revision 20261110

grr chat

Forms

Form bodies, responses, publish settings, and push-notification watches.

10 methods · v1 · revision 20261108

grr forms

Sheets

Spreadsheet values, batch updates, data filters, and developer metadata.

17 methods · v4 · revision 20261111

grr sheets

Tasks

Task lists and the tasks inside them. See the scope caveat below.

14 methods · v1 · revision 20261110

grr tasks

Slides

Presentation batch updates, page reads, and thumbnails.

5 methods · v1 · revision 20261109

grr slides

Docs

Document create, read, and batch update.

3 methods · v1 · revision 20261109

grr docs

Apps Script

Script projects, deployments, versions, processes, and running functions.

16 methods · v1 · revision 20261106

grr script

Analytics Admin

Accounts, properties, data streams, custom dimensions and metrics, conversion events, and the Ads and Firebase links.

55 methods · v1beta · revision 20261111

grr analyticsadmin

Analytics Data

Run reports — standard, realtime, pivot, and batch — plus compatibility checks, metadata, and audience exports.

11 methods · v1beta · revision 20261111

grr analyticsdata

Search Console

Search analytics queries, sitemaps, sites, and URL inspection.

11 methods · v1 · revision 20261110

grr searchconsole

Every service in the index has a full generated command namespace now — the cards above are the namespaces. grr api is not the only way into any of them; it is the flat alternative when you would rather compose an id than walk a path.

call

How parameters become a request.

One rule covers every method: path placeholders are substituted into the URL, and everything else becomes a query parameter or the request body.

A missing required parameter is caught before anything is sent, and the error names the parameter and lists the rest, rather than letting Google answer with a 404 or a confusing 400.

  • --params JSON takes a whole object. Must be a JSON object; an array or a bare value is rejected with a message saying so.
  • --param KEY=VALUE is repeatable and wins over --params, so you can override one field of a large blob. Values are coerced to real JSON types: --param pageSize=10 is a number, not the string "10".
  • Only the first = splits, so --param 'q=name=a.pdf' and Gmail search strings survive intact.
  • --body-file PATH reads the body verbatim from a file, or from stdin with -, and wins over anything --params would have synthesised. This is the path for nested bodies.
  • --query KEY=VALUE appends raw query pairs for parameters Discovery does not describe, such as alt=json or fields.
  • --method-override VERB replaces the verb from the Discovery document, for the rare endpoint where the published method is behind the live API.

For POST, PATCH, and PUT without --body-file, the non-path parameters are collected into a JSON body automatically — and they are also appended as query pairs, which the dry-run output above shows honestly. For read verbs no body is sent at all. Path values are percent-encoded, so Drive ids and Gmail labels containing spaces or slashes are safe.

grr api call
# Path parameters are substituted into the URL; the rest become
# query parameters (GET) or the request body (POST/PATCH/PUT).
$ grr api call gmail.users.messages.list --param userId=me --param 'q=is:unread' --param maxResults=5
$ grr api call drive.files.list --params '{"pageSize": 10, "q": "name contains \'report\'"}'

# Values are coerced to real JSON types, so numbers and booleans need no quoting.
$ grr api call sheets.spreadsheets.values.get \
    --param spreadsheetId=1a2b3c --param range=A1:D10

# A body from a file ("-" reads stdin), sent verbatim.
$ grr api call chat.spaces.messages.create \
    --param parent=spaces/AAAA --body-file ./message.json

# Endpoints whose parameters Discovery does not describe take raw query pairs.
$ grr api call drive.files.list --query alt=json --query fields=files(id,name)

# Override the verb when the Discovery document is behind the live API.
$ grr api call tasks.tasks.list --method-override GET

Inspect before you send

--dry-run prints the request that would be sent and sends nothing. It is the cheapest way to find out whether a call is shaped correctly.

The output includes the resolved method, the fully built url, the body that would be serialised, the scopes the method declares, the narrowest of them, and dryRun: true. Because URL construction and parameter validation both happen before the dry-run branch, a dry run surfaces exactly the errors a real call would.

--dry-run · both routes
$ grr api call calendar.events.insert \
    --param calendarId=primary \
    --params '{"summary":"Standup","start":{"dateTime":"2026-10-05T09:30:00Z"},"end":{"dateTime":"2026-10-05T09:45:00Z"}}' \
    --dry-run
{
  "method": "POST",
  "url": "https://www.googleapis.com/calendar/v3/calendars/primary/events?summary=Standup",
  "body": {"summary":"Standup","start":{...},"end":{...}},
  "scopesRequested": ["https://www.googleapis.com/auth/calendar", "..."],
  "leastPrivilegeScope": "https://www.googleapis.com/auth/calendar.events.owned",
  "dryRun": true
}

# With --body-file the body is sent verbatim and the URL stays clean:
$ grr api call calendar.events.insert --param calendarId=primary \
    --body-file ./event.json --dry-run

# Every generated leaf has the same --dry-run, so the same inspection works
# without composing the id by hand:
$ grr calendar events insert --calendar-id primary --dry-run

The flag exists on every generated leaf as well as on grr api call, and both print JSON — so a jq -e '.url' assertion works on either.

Method ids and typo handling

A method id is the dotted service.resource.method path, taken verbatim from the Discovery document — resource segments included, so gmail.users.messages.list is the id, not gmail.messages.list. The service prefix is the same key --service takes, and slashes are accepted in place of dots. The same id, with dots as spaces, is the generated command; the tree and grr api are two views of one index.

method ids
# Canonical: <service>.<resource>.<method>
$ grr api call drive.files.list

# Slashes work too, and a leading dot is tolerated.
$ grr api call gmail/users/messages/list
$ grr api call .gmail.users.messages.list

# A typo gets ranked suggestions rather than a bare "not found".
$ grr api call gmail.users.messages.lst
unknown method 'users.messages.lst'; did you mean: users.messages.list, ...

An unknown service lists the 14 known keys. An unknown method is matched against the real ids and returns ranked suggestions, preferring candidates that share the most of the resource path — which is what tells users.messages.list apart from users.drafts.list when both are one typo away.

docs · sheets · slides

The small services, in practice.

Three APIs that no CLI used to wrap. Generated from the same index, they now have full command paths.

docs · sheets · slides
# Sheets: read a range, write a range, append a row.
$ grr sheets spreadsheets values get --spreadsheet-id 1a2b3c --range Sheet1!A1:D10
$ grr sheets spreadsheets values update --spreadsheet-id 1a2b3c --range Sheet1!A1 \
    --body-file ./values.json
$ grr sheets spreadsheets values append --spreadsheet-id 1a2b3c --range Sheet1!A:A \
    --value-input-option USER_ENTERED

# Docs: create, read, and batch-update.
$ grr docs documents get --document-id 1a2b3c
$ grr docs documents create --body-file ./new-doc.json
$ grr docs documents batchUpdate --document-id 1a2b3c --body-file ./requests.json

# Slides: presentations, pages, and thumbnails.
$ grr slides presentations get --presentation-id 1a2b3c
$ grr slides presentations pages getThumbnail \
    --presentation-id 1a2b3c --page-object-id g1

Note the colon-verb path templates — values/{range}:append, documents/{documentId}:batchUpdate. The braces are path placeholders, so --range Sheet1!A1:D10 substitutes into the URL and is percent-encoded for you, colons and all.

Sheets and Slides writes need the request body as a nested structure, which is what --body-file is for. Generating that JSON in a heredoc and piping it through stdin (--body-file -) keeps it out of shell-quoting hell. And camelCase methods keep their casing as leaf aliases: getThumbnail, not get-thumbnail.

authorization

A small consent by default, widened per service.

grr requests Gmail, Drive, Calendar, Tasks, Docs, Sheets, Slides and the sign-in scopes at login, then grr auth login --service <name> adds one service's scopes from the same index — because Google's consent screen stops submitting past roughly 27 ticked boxes on an unverified project. Every login merges with the account's existing grant, so widening never drops access.

Discovery lists each method’s accepted scopes from broadest to narrowest, usually with https://mail.google.com/ or .../auth/drive first. grr api describe reports the narrowest entry as leastPrivilegeScope, which is the right thing to show a human deciding what a call touches.

Reporting is the only place scope shows up during a call, though. There is no per-method consent gate: grr auth login asks for a small default set and --service <name> widens it from the same index, so a call is never compared against the set or annotated when a narrow scope differs from a broader one you hold — and a service you have not asked for yet is one command away.

That leaves one honest failure mode: the scope a method declares may not be in the credential’s grant yet. grr names the scope it asked for and points at the remedy:

tasks · a 403 after the grant moved
$ grr api list --service tasks
14 methods

$ grr api describe tasks.tasks.list
# leastPrivilegeScope: https://www.googleapis.com/auth/tasks.readonly
#   -> in the default grant: Tasks is one of the services login covers

$ grr tasks tasks list --tasklist @default
GET https://tasks.googleapis.com/tasks/v1/users/@me/lists -> HTTP 403: this
method needs the `https://www.googleapis.com/auth/tasks.readonly` scope.
Tasks is inside the default grant this build asks for, so a 403 here means the
API is not enabled on the Cloud project, or the grant was narrowed. Services
outside the default set (chat, forms, searchconsole) need
`grr auth login --service <service>` to widen it.

A service outside the default set is one command away. Tasks, Analytics Admin, Analytics Data, Search Console, and Apps Script (including scripts.run) all declare scopes that grr auth login --service <name> requests, so a live call succeeds as soon as the API is enabled on your Cloud project. Gmail CSE keypairs, S/MIME settings, directory people, and the Chat admin surface come in the same way. The one deliberate exception is Chat’s app-authentication surface (chat.bot, chat.app.*, chat.import): Google grants those to a service account, and a consent screen that asks for one is rejected outright, so they are filtered out — the methods behind them authenticate as the app, never as you. A residual 403 names the scope it wanted and the widening command for it.

The trade-off sits on the consent screen, and it flipped: a small list to approve, plus one command when a task reaches past it. --all-scopes still asks for the whole index in one go, which is the right call on a verified project where a long list is only a long page.

How the index stays current

The index is committed at src/discovery/*.json so builds stay hermetic and grr api list works offline. Something has to notice upstream drift, and that is a scheduled workflow: a daily run refetches the 14 Discovery documents, distils them with scripts/fetch-discovery.ts, regenerates the command tree with scripts/generate-commands.ts, the agent skills with scripts/generate-skills.ts, and this page’s coverage table with scripts/generate-coverage.ts, then opens a pull request when any of them differ. An unchanged upstream produces an empty diff and no PR.

Every output is deterministic (sorted keys, no wall-clock) precisely so that comparison is meaningful. When the index does change, the workflow runs the Rust test suite against the new data before proposing the change, so a method whose shape broke incompatibly fails the build instead of shipping. The index, the command tree, the skills, and this coverage table all move in the same PR, so the docs, the commands, and the index can never drift apart.