Skip to content

Grant an App access to a connection

CLIAPIIntermediate~10 min

You have connected a mailbox, calendar or drive. An App — a headless assistant, a scheduling agent — needs to act on part of it. This page issues that access, checks it, and revokes it.

You must own the connection. There is no org-admin path: an administrator cannot grant on your connection for you, and cannot revoke a grant you made.

Step 1 — Find the connection

The grant commands take a reference to a connection you already have, and there is no owner-scoped way to list connections — get the id from the portal, where you connected it.

That is a prerequisite, not a step of the task: granting, checking and revoking all happen on the CLI or the API, which is what this page is badged for.

The API listing is an admin operation, not the owner's route

exchangeConnections(orgId:) exists, but listing and deleting connections are org ADMIN management operations. Operating a mailbox is owner-only; enumerating an org's connections is administration. So unless you are also an org ADMIN, that query will not return your own connection, and the portal is the way in.

Step 2 — Decide the scopes

A grant carries one or more scopes, and each operation an agent runs maps to exactly one:

Scope What it lets the grantee do
mail.read List messages and folders
mail.send Reply to a message
calendar.freebusy Ask whether you are busy — not what you are doing
calendar.read List calendars and read events, including their contents
drive.read List, search, fetch, export and download files

Grant the narrowest set that does the job, and note that calendar.freebusy is not a weaker calendar.read — it answers a different question, and is the right scope for anything that only needs to schedule.

Replying is the only write a grant can carry. Every other mutation is owner-only — nothing a grant holds reaches:

  • mail — deleting, moving, marking read, flagging, categorizing
  • calendar — creating, updating, cancelling or responding to events
  • drive — creating documents

If an agent needs one of those, delegation is not the mechanism. The pattern is worth holding rather than the list: a grantee reads, and answers mail. Anything that changes your data in any other way stays with you.

Drafts: treat as owner-only until confirmed

The draft operations (save, update, send) sit in an unresolved spot. The operation→scope table maps them to mail.send, while the GraphQL mutations that perform them (emailSaveDraft, emailUpdateDraft, emailSendDraft) are each documented "Owner-only."

Those two describe different layers and may both be right, but until that is settled this page will not tell you a grantee can drive drafts. Plan for owner-only — the stricter reading is the safe one to be wrong about.

Step 3 — Create the grant

hadron connection grant create \
  --connection <connection-id> \
  --app hrn:app:acme.com:scheduling-assistant \
  --scopes calendar.freebusy

--app takes an App id or URN. Without --expires-at the grant lasts until you revoke it. To bound it, pass an ISO-8601 timestamp in the future — computed, so the example cannot go stale:

# 90 days out; the first form is BSD/macOS, the fallback is GNU/Linux
EXPIRY=$(date -u -v+90d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
      || date -u -d '+90 days' +%Y-%m-%dT%H:%M:%SZ)

hadron connection grant create \
  --connection <connection-id> \
  --app hrn:app:acme.com:scheduling-assistant \
  --scopes calendar.freebusy \
  --expires-at "$EXPIRY"

Prefer an expiry when you know the end date

A contractor engagement or a trial has a date. An expiry you set once beats a revocation you have to remember, and it fails in the safe direction if you forget.

Adding a scope later does not mean editing this grant. Multiple live grants on the same connection and App are valid and their scopes union, so you can issue a second grant for the extra scope and revoke it independently.

Step 4 — Check what you have granted

hadron connection grant list --connection <connection-id>

Run it without --connection to see every grant you have issued. This is the review surface — the answer to "what can that assistant currently see?" — so it is worth a look after any change, and periodically after none.

Step 5 — Revoke

hadron connection grant revoke <grant-id> --yes

Revocation takes effect immediately: liveness is re-checked on every operation rather than cached, so there is no window in which a revoked grant still works. It is a soft delete, so the record of what was granted survives for audit.

Revoking a grant does nothing to the connection. It stays connected and everything else keeps working.

Doing it through the API

mutation($conn: String!, $app: String!, $scopes: [String!]!) {
  createConnectionGrant(connectionRef: $conn, appRef: $app, scopes: $scopes) {
    id scopes expiresAt
  }
}

connectionGrants(connectionRef:) lists them and revokeConnectionGrant(ref:) takes one back. Same gate as the CLI: owner-only, both ways.

When something refuses

What you see What it means
Refused on create, and you are not the connection's owner Correct. Owner-only, with no admin bypass. Ask the owner to issue it.
An unrecognised scope is rejected The scope list is a fixed allow-list — check the spelling against the table above.
The agent still cannot perform an operation it has the scope for Check the operation is delegable at all: the owner-only set above is refused for every grantee, whatever scopes they hold.
A grant you expected is missing from grant list It expired or it was revoked — two different things. Revocation is a soft delete (the row is marked deleted); expiry just means expiresAt has passed, and the row is untouched. Neither is an edit, so the fix is a new grant, not an undo.