# Connections

> Let functions act on the account of the person running them, such as their Gmail or calendar, with your own OAuth app.

Source: https://callables.app/docs/connections

Connections let a function act on the account of the person running it: their Gmail, their calendar, their Slack. Each person signs in once with the service, and every run they start gets a fresh access token for their own account.

Use [secrets](https://callables.app/docs/secrets) when a function should use one shared account, such as your company's Stripe key. Use a connection when each person should use their own.

The services you can connect are **connectors**. Each has a setup guide on the [connectors page](https://callables.app/connectors): [Google](https://callables.app/connectors/google), [Microsoft](https://callables.app/connectors/microsoft), [Slack](https://callables.app/connectors/slack), [GitHub](https://callables.app/connectors/github), [Notion](https://callables.app/connectors/notion) and [any other OAuth 2.0 service](https://callables.app/connectors/any-oauth-service). This page is the reference for declaring and using them.

## How it works

You bring the OAuth app, and Callables runs everything around it:

| You | Callables |
|---|---|
| Create an OAuth app with the service and choose its scopes | Hosts the redirect, so people can sign in |
| Put the app's client ID and secret in project secrets | Stores each person's tokens encrypted, and refreshes them |
| Declare the connection in code and read its token | Gives each run a token for the person who started it |
| | Asks people to connect, reconnect or approve new scopes |

Because the app is yours, you choose any scopes the service offers, and the sign-in screen shows your app's name, so people see exactly who they're granting access to.

## Declare a connection

```python
from callables import Connection, deploy

import requests

google = Connection("google", provider="google", scopes=["gmail.modify", "calendar.readonly"])


@deploy(title="Triage my inbox", connections=[google])
def triage_inbox(max_messages: int = 25) -> str:
    """Labels your newest unread emails by topic."""
    headers = {"Authorization": f"Bearer {google.token}"}
    resp = requests.get("https://gmail.googleapis.com/gmail/v1/users/me/messages",
                        headers=headers, params={"q": "is:unread", "maxResults": max_messages})
    ...
```

- The first argument is the connection's name. It's how Callables tells connections apart in a project, so **renaming it makes everyone connect again**.
- `provider` picks a preset that knows the service's sign-in details. See [providers](#providers).
- `scopes` are the permissions you ask for. Presets accept short names, so `"gmail.modify"` means `https://www.googleapis.com/auth/gmail.modify`. Full scope URLs work too.
- Declaring a `Connection` at module level is cheap and makes no network calls, so it's safe at import time.

Inside the function, read these properties. They only have values during a run:

| Property | What it is |
|---|---|
| `google.token` | An access token for the person running the function, valid for the whole run |
| `google.account` | Which account they connected, such as `alice@company.com`, when the service says |
| `google.scopes_granted` | The scopes they granted |
| `google.extra` | Other values the service returned at sign-in, such as Salesforce's `instance_url` |

The token is also in the environment as `<NAME>_ACCESS_TOKEN` (here `GOOGLE_ACCESS_TOKEN`), for tools that read it from there.

A module-level object whose token belongs to whoever is running it is safe here because every run is its own fresh process: one run, one person. Don't copy this pattern into a long-running web server.

A [group](https://callables.app/docs/groups) can declare connections once for every action in it: `Group("mail", connections=[google])`.

## Set up the OAuth app

Once per project:

1. Create an OAuth app (sometimes called a client or an app registration) in the service's developer console.
2. Add this redirect URI: `https://callables.app/connections/callback`
3. Add the scopes your code asks for, if the console asks you to list them.
4. In the project's secrets, set the app's client ID and secret. By default they're named after the connection: `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` for a connection named `google`. To pick the names, pass `client_id=Secret("...")` and `client_secret=Secret("...")`. See [secrets and values](#secrets-and-values).
5. Push, then open the project page. Its **Connections** card shows the redirect URI to copy, any missing secrets, and a **Connect** button to try the connection with your own account.

The redirect goes to the Callables web app, which hands the code to the API with your signed-in session. So a connection is always saved for whoever is signed in, and nobody can attach their account to someone else's.

Whether the service needs to review your app is between you and the service. Apps only your own organization uses usually don't need review: for Google, set the app's user type to **Internal** on a Google Workspace account. Google apps left in **Testing** status get tokens that expire after 7 days, so people would have to reconnect every week. An External Google app can also be switched to **In production** without verification: people see an "unverified app" warning, and it's capped at 100 users.

Connections are built for apps your own team uses. Google's verification, for a public app with no warning or user cap, requires the redirect URI's domain to be one you own, and the redirect is on `callables.app`, so verified Google apps aren't supported yet.

## What people see

The first time someone opens a function that needs a connection, they see what it uses and a **Connect** button instead of Run:

- **Connect Google** opens the service's own sign-in screen, showing your app's name and the permissions it asks for.
- After they allow it, they're sent back and can run the function.
- If they untick a permission on the consent screen (Google lets people do this for Gmail, Drive and others), the connection is refused with a message naming what's missing, so they never end up connected without the access the function needs.
- Other functions in the same project that use the same connection run right away.
- If you add scopes, their next run asks them to **Reconnect**, and the service asks only for the new permissions.
- If they remove access from the service's side, the function asks them to reconnect instead of failing during the run.

Each person manages their connections on the **Connections** page in the sidebar: which account each one uses, which project it's for, its permissions and when it was last used. **Disconnect** deletes the tokens from Callables and revokes them with the service.

## Whose account a run uses

| How the run started | Connection it uses |
|---|---|
| Someone runs it from the form | Theirs |
| A schedule | The person who made the schedule |

A run never uses another person's connection. If the person it runs as hasn't connected, it doesn't start, and they're asked to connect. That's different from secrets, where a missing secret doesn't block the run.

Builders never see anyone's tokens in the web app. Your code does receive them during a run, so only ask for the scopes the function needs.

## Security

- Tokens are encrypted at rest, and only the API and the run workers decrypt them.
- A token only reaches runs started by the person it belongs to, or by schedules they made.
- Tokens are hidden in run logs, like secrets.
- **Disconnect** deletes the tokens and, where the service allows it, revokes them.
- Removing a connection from your code disconnects everyone who connected it, on the next deploy.
- The URLs a connection uses must be public `https` addresses.

## Providers

These presets fill in each service's sign-in details:

| Provider | Covers | Scope examples |
|---|---|---|
| [`google`](https://callables.app/connectors/google) | Gmail, Calendar, Drive, Sheets, Docs, Contacts | `gmail.modify`, `calendar.readonly`, `drive.file` |
| [`microsoft`](https://callables.app/connectors/microsoft) | Outlook mail and calendar, OneDrive, Teams, Excel, SharePoint | `Mail.ReadWrite`, `Calendars.Read`, `Files.ReadWrite` |
| [`slack`](https://callables.app/connectors/slack) | Messages, channels, users | `chat:write`, `channels:read` |
| [`github`](https://callables.app/connectors/github) | Repos, issues, pull requests | `repo`, `read:org` |
| [`notion`](https://callables.app/connectors/notion) | Pages and databases the person shares with your app | none (Notion sets access when they connect) |

Presets handle each service's quirks for you. For example, `google` asks for offline access so the connection lasts, `microsoft` adds `offline_access` to the scopes, and `slack` asks for a user token so the run acts as the person.

Microsoft's URLs contain `{tenant}`, which is `common` unless you fill it in. `common` works for multi-tenant apps. An app only your own organization uses (single tenant) needs its **Directory (tenant) ID**, from a project secret:

```python
outlook = Connection(
    "outlook",
    provider="microsoft",
    scopes=["Mail.ReadWrite"],
    client_id=Secret("MS_CLIENT_ID"),
    client_secret=Secret("MS_CLIENT_SECRET"),
    values={"tenant": Secret("MS_TENANT_ID")},
)
```

See the [Microsoft connector guide](https://callables.app/connectors/microsoft).

## Secrets and values

A connection never has your app's details written in its code. They come from **secrets**:

- **Client ID and secret.** By default a connection reads the project secrets `<NAME>_CLIENT_ID` and `<NAME>_CLIENT_SECRET`, named after the connection, so `Connection("google", ...)` reads `GOOGLE_CLIENT_ID`. Name your own with `client_id=Secret("MS_CLIENT_ID")` and `client_secret=Secret("MS_CLIENT_SECRET")`. Several connections can then share one app's secrets.
- **Values in the URLs.** Any `{placeholder}` in a connection's URLs or `authorize_params` is filled from `values`. That's how one `Connection` covers services with their own domains, tenants or regions, without an SDK update.

| Value | Where it comes from | Use it for |
|---|---|---|
| `Secret("NAME")` | The project secret `NAME` on Callables, and the environment variable (or `.env`) locally | Anything that's the same for everyone: a tenant ID, an Okta or Auth0 domain, a region |
| `Secret("NAME", default="...")` | The same, or the default when it isn't set | An optional setting, such as Microsoft's tenant (`common`) |
| `Ask("prompt")` | The person connecting: the Connect button shows a text box with the prompt, and `call connect` asks in the terminal. Each person's answer is saved with their connection | Something that differs per person, such as their Shopify store |
| A plain string | Your code | A constant that never changes |

`Secret` is a reference to the secret, not its value. Callables reads connection settings when it builds, before secrets exist, and signs people in without running your code, so it needs the name and looks the value up itself.

Every placeholder must be filled, and every value must match a placeholder. A typo fails at import, so `call check` catches it before you push. Filled-in values must be plain names (letters, digits, `.` and `-`), so they can't change the shape of a URL.

## Any other OAuth service

For a service without a preset, give its URLs instead of `provider`. Most services only need the first three:

```python
crm = Connection(
    "crm",
    authorize_url="https://login.somecrm.com/oauth/authorize",
    token_url="https://login.somecrm.com/oauth/token",
    scopes=["contacts.read"],
)
```

When a service does things its own way, these options cover it:

| Option | Default | Use it when |
|---|---|---|
| `authorize_params` | none | The service needs extra values on the sign-in link, such as `{"audience": "api.example.com"}` |
| `token_endpoint_auth_method` | `"client_secret_post"` | The service wants the client ID and secret as HTTP Basic auth: set `"client_secret_basic"` |
| `token_request_format` | `"form"` | The service wants the token request as JSON: set `"json"` |
| `scope_separator` | `" "` | The service separates scopes with commas: set `","` |
| `scope_param` | `"scope"` | The service reads scopes from another parameter, such as Slack's `user_scope` |
| `pkce` | `False` | The service requires PKCE |
| `token_path` | top level | The token sits deeper in the response, such as `"authed_user.access_token"` |
| `userinfo_url` | none | A URL that returns the signed-in account, so people see which one they connected |
| `account_field` | `"email"` | The field that names the account: in `userinfo_url`'s response, or in the token response when there's no `userinfo_url` |
| `revoke_url` | none | Where to revoke a token when someone disconnects |
| `values` | none | The URLs or `authorize_params` contain `{placeholders}`. See [secrets and values](#secrets-and-values) |

A service on your own domain, such as Okta, fills the domain from a secret:

```python
okta = Connection(
    "okta",
    authorize_url="https://{domain}/oauth2/v1/authorize",
    token_url="https://{domain}/oauth2/v1/token",
    scopes=["openid", "email"],
    values={"domain": Secret("OKTA_DOMAIN")},
)
```

A service where each person has their own instance, such as Shopify, asks them:

```python
shop = Connection(
    "shopify",
    authorize_url="https://{shop}.myshopify.com/admin/oauth/authorize",
    token_url="https://{shop}.myshopify.com/admin/oauth/access_token",
    scopes=["read_orders"],
    scope_separator=",",
    values={"shop": Ask("Your store name, the part before .myshopify.com")},
)
```

Callables refreshes tokens on its own, keeps the new refresh token when a service changes it on every refresh, and treats a token with no expiry as long-lived. Connections need OAuth 2.0. For a service that only gives out API keys, use [secrets](https://callables.app/docs/secrets).

## Locally

Sign in once on your machine, and `call local` and `call <function>` run with your own account:

```bash
call connect google
```

1. Put the connection's secrets in the project's `.env` (or one at the top of the repo): the client ID and secret, and any `Secret(...)` values. They're the same names and values as the project's secrets.
2. Add `http://localhost:8765/callback` as a second redirect URI on the app, next to the Callables one.
3. Run `call connect` and pick the connection (or `call connect <name>`). It opens the service's sign-in in your browser, catches the redirect on your machine, and checks that every permission was granted.

Nothing goes through Callables. The tokens are kept in your user config folder (`~/.config/callables/` on Linux and macOS), readable only by you, and refreshed when they're about to expire. This local connection is separate from the one you make in the web app. `call disconnect <name>` forgets it.

A token you set yourself wins: put one in `.env` or your shell as `<NAME>_ACCESS_TOKEN`, for CI or a token from elsewhere. `--port` uses another port for the redirect, and `--no-browser` prints the sign-in link instead of opening it.

`call check` catches problems before you push, such as an unknown provider, a URL that isn't `https`, or one connection name defined twice with different settings. It also lists the secrets each connection reads.

## Rules

- Read `.token` inside the function, never at module level. There's no token at build time.
- Never print or log a token. Callables hides known token values in logs, but don't rely on it.
- Ask for the narrowest scopes that work, such as `drive.file` instead of `drive`.
- Don't take a token, password or account email as a form input. Use a connection.
- Use one connection per service in a project, and put every scope the project needs on it, so people only connect once.
