# Connections

> Owner-scoped credentials bound to integrations, credential provider resolution, OAuth minting, and the `(owner, integration, name)` identity model.

- Repository: RhysSullivan/executor
- GitHub: https://github.com/RhysSullivan/executor
- Human docs: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052
- Complete Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/llms-full.txt

## Source Files

- `apps/docs/concepts/connections.mdx`
- `packages/core/api/src/connections/api.ts`
- `packages/core/api/src/handlers/connections.ts`
- `packages/core/sdk/src/connection.ts`
- `packages/core/sdk/src/oauth-service.ts`
- `examples/docs-sdk-quickstart/src/main.ts`

---

---
title: "Connections"
description: "Owner-scoped credentials bound to integrations, credential provider resolution, OAuth minting, and the `(owner, integration, name)` identity model."
---

A connection is the saved credential for one integration: owner-scoped, bound 1:1 to an integration slug, with its secret value stored in a `CredentialProvider` and applied lazily per tool call. Creating a connection (`executor.connections.create` or `POST /connections`) writes the routing row, stores pasted values in the default writable provider (or references an external provider item), and produces that connection's tool catalog. OAuth connections are minted through `executor.oauth.start` / `complete` instead.

## Identity model

Every connection is uniquely identified by three fields:

| Field | Type | Role |
| --- | --- | --- |
| `owner` | `"org"` \| `"user"` | Workspace-shared (`org`) or personal (`user`) partition |
| `integration` | `IntegrationSlug` | The integration this credential is for |
| `name` | `ConnectionName` | Distinct name under that owner + integration |

The SDK type is `ConnectionRef`:

```ts
interface ConnectionRef {
  readonly owner: Owner;
  readonly name: ConnectionName;
  readonly integration: IntegrationSlug;
}
```

Connection names are normalized to JS-callable identifiers on create. Free-form input like `my-api-key` becomes `myApiKey` via `connectionIdentifier`.

<Note>
`owner: "user"` writes require a signed-in subject. Creating a personal connection without a user subject returns `InvalidConnectionInputError`.
</Note>

### Address format

Each connection exposes a callable address:

```
tools.<integration>.<owner>.<connection>
```

Append `.<tool>` for a full tool address:

```
tools.<integration>.<owner>.<connection>.<tool>
```

Example: `tools.inventory.org.default.listItems`.

## Connection record

The `Connection` shape returned by list, get, create, and OAuth mint:

<ResponseField name="owner" type="Owner">
Workspace (`org`) or personal (`user`) owner.
</ResponseField>

<ResponseField name="name" type="ConnectionName">
Normalized connection name.
</ResponseField>

<ResponseField name="integration" type="IntegrationSlug">
Integration slug this credential is bound to.
</ResponseField>

<ResponseField name="template" type="AuthTemplateSlug">
Which auth method on the integration this credential applies through (for example `apiKey`, `bearer`, or `none`).
</ResponseField>

<ResponseField name="provider" type="ProviderKey">
Credential backend key (`memory`, `default`, `1password`, `keychain`, etc.). Routing only; never the secret itself.
</ResponseField>

<ResponseField name="address" type="ConnectionAddress">
Callable handle: `tools.<integration>.<owner>.<connection>`.
</ResponseField>

<ResponseField name="identityLabel" type="string | null">
Optional human label (which account). Not load-bearing.
</ResponseField>

<ResponseField name="description" type="string | null">
User-curated, agent-visible context (for example "staging CRM, reads only"). Surfaces in tool inventory and `connections.list`.
</ResponseField>

<ResponseField name="expiresAt" type="number | null">
Epoch ms when an OAuth access token expires. Null for static credentials.
</ResponseField>

<ResponseField name="oauthClient" type="OAuthClientSlug | null">
OAuth app slug that minted this connection. Null for static credentials.
</ResponseField>

<ResponseField name="oauthClientOwner" type="Owner | null">
Owner of the OAuth app (may differ from the connection owner when a personal connection uses a workspace app).
</ResponseField>

<ResponseField name="oauthScope" type="string | null">
Space-delimited scopes the provider granted at connect/refresh. Compared against the integration's declared scopes to decide whether reconnect is needed.
</ResponseField>

## Born wired

A connection is created already bound to one integration. There is no separate "connect" step and no unwired state. On create:

1. Validate the integration exists and credential inputs are well-formed.
2. Resolve value origins to one provider and an `item_ids` map (`variable → provider item id`).
3. Upsert the `connection` row (re-create with the same `(owner, integration, name)` updates credentials in place).
4. Call the integration plugin's `resolveTools` and persist the connection's tool rows.

Credentials are applied to the integration's auth template lazily at invoke time, never pre-baked into headers or URLs ahead of time.

## Owner scopes

| Owner | Visibility | Typical use |
| --- | --- | --- |
| `org` | Tenant-shared; `subject` is empty | Team API keys, shared OAuth accounts |
| `user` | Scoped to the acting member's subject | Personal tokens, BYO credentials |

One integration can have many connections under the same owner (for example `default` and `staging` on `inventory`). The same connection name can exist on different integrations because `integration` is part of the identity key.

## Credential origins

Static connections accept exactly one origin form. The HTTP API enforces this with `"Expected exactly one credential origin"`.

<ParamField body="value" type="string">
Sugar for a single `token` input. Written to the default writable provider.
</ParamField>

<ParamField body="values" type="Record<string, string>">
Multi-input map (one entry per named variable). All values go to the default writable provider.
</ParamField>

<ParamField body="from" type="{ provider: ProviderKey; id: ProviderItemId }">
External provider reference. The value is resolved on demand via `provider.get(id)` and is never copied into core storage.
</ParamField>

<ParamField body="inputs" type="Record<string, ConnectionInputOrigin>">
Canonical per-variable origin map. Each entry is `{ value }` or `{ from: { provider, id } }`. SDK-only; mixes pasted and external in one map.
</ParamField>

### Provider resolution rules

| Rule | Behavior |
| --- | --- |
| Single provider per connection | All inputs of one connection share one `provider` key on the row |
| Pasted inputs | Stored in the first registered writable provider (`defaultWritableProvider`) |
| External inputs | Routed to the named provider; `item_ids` holds opaque provider item ids |
| Mixed origins | Rejected: cannot mix pasted `value`/`values` with external `from` |
| Multiple external providers | Rejected: all external inputs must use the same provider |
| Missing writable provider | `CredentialProviderNotRegisteredError` (HTTP 409) |

Pasted values are stored under ids like `connection:{owner}:{integration}:{name}:{variable}`. OAuth access tokens use `oauth:{owner}:{integration}:{name}` with refresh tokens at `{id}:refresh`.

At invoke time, `resolveConnectionValues` loads each `item_ids` entry through the registered provider. OAuth connections refresh the access token first when `expiresAt` is within the skew window, then return `{ token: <access> }`.

<Warning>
External `from` references that resolve to `null` are allowed at create time. The failure surfaces at invoke time as `connection_value_missing`, not during create.
</Warning>

## No-auth connections

Integrations with no credential requirement use `template: "none"` (`NO_AUTH_TEMPLATE`). These connections legitimately bind zero inputs; the persisted `item_ids` map is empty.

```ts
await executor.connections.create({
  owner: "org",
  name: "public",
  integration: "context7",
  template: "none",
  // No value, from, values, or inputs required
});
```

Credentialed templates reject empty `values: {}` or `inputs: {}` at create with `InvalidConnectionInputError` (HTTP 400). An empty-string `value` is allowed when a binding must exist but carries no secret.

## Create a static connection

<Steps>
<Step title="Register the integration">
Add the integration first via `openapi.addSpec`, `graphql.addIntegration`, `mcp.addServer`, or a plugin extension. The integration declares auth templates (where credentials render on requests).
</Step>

<Step title="Register credential providers">
Pass writable providers to `createExecutor({ providers: [...] })`. The first writable provider in registration order receives pasted values.
</Step>

<Step title="Create the connection">
Call `executor.connections.create` with `owner`, `name`, `integration`, `template`, and one credential origin.
</Step>

<Step title="Verify tools">
List tools filtered by integration. Each tool address includes the connection segment.
</Step>
</Steps>

<CodeGroup>
```ts SDK (inline value)
await executor.connections.create({
  owner: "org",
  name: "default",
  integration: "inventory",
  template: "apiKey",
  value: "inventory-api-key",
});
```

```ts SDK (external reference)
await executor.connections.create({
  owner: "org",
  name: "prod",
  integration: "inventory",
  template: "apiKey",
  from: {
    provider: "1password",
    id: "op://vault/item/field",
  },
});
```

```http HTTP API
POST /connections
Content-Type: application/json

{
  "owner": "org",
  "name": "default",
  "integration": "inventory",
  "template": "apiKey",
  "value": "inventory-api-key"
}
```
</CodeGroup>

<RequestExample>
```json
{
  "owner": "org",
  "name": "default",
  "integration": "inventory",
  "template": "apiKey",
  "value": "inventory-api-key",
  "description": "Production inventory API, read-only"
}
```
</RequestExample>

<ResponseExample>
```json
{
  "owner": "org",
  "name": "default",
  "integration": "inventory",
  "template": "apiKey",
  "provider": "memory",
  "address": "tools.inventory.org.default",
  "identityLabel": null,
  "description": "Production inventory API, read-only",
  "expiresAt": null,
  "oauthClient": null,
  "oauthClientOwner": null,
  "oauthScope": null
}
```
</ResponseExample>

## OAuth minting

OAuth is a credential mechanism, not an integration type. Do not use `connections.create` for OAuth tokens. Instead:

1. Register an owner-scoped OAuth app (`oauth.createClient` or `oauth.registerDynamicClient`).
2. Start a flow (`oauth.start`) for a target `(owner, integration, name, template)`.
3. Complete the flow (`oauth.complete`) after the user authorizes (or receive an immediate `connected` result for `client_credentials`).

```mermaid
sequenceDiagram
  participant UI as Web UI / Agent
  participant API as Executor runtime
  participant AS as Authorization server
  participant CP as Credential provider

  UI->>API: oauth.start(client, clientOwner, owner, integration, name, template)
  alt client_credentials grant
    API->>AS: Token exchange
    AS-->>API: access_token
    API->>CP: set oauth:owner:integration:name
    API-->>UI: { status: "connected", connection }
  else authorization_code grant
    API->>API: Persist oauth_session (PKCE + state)
    API-->>UI: { status: "redirect", authorizationUrl, state }
    UI->>AS: User authorizes
    AS-->>UI: Redirect with code
    UI->>API: oauth.complete(state, code)
    API->>AS: Code exchange
    AS-->>API: access_token (+ refresh_token)
    API->>CP: set access + refresh item ids
    API->>API: mintOAuthConnection (row + tools)
    API-->>UI: Connection
  end
```

### OAuth start result

| `status` | Meaning | Next step |
| --- | --- | --- |
| `connected` | Token exchanged inline (`client_credentials`) | Connection is ready; tools are produced |
| `redirect` | User must visit `authorizationUrl` | Complete with `state` + `code` after callback |

<ParamField body="client" type="OAuthClientSlug" required>
OAuth app slug to run the flow through.
</ParamField>

<ParamField body="clientOwner" type="Owner" required>
Owner of the OAuth app. A workspace connection (`owner: "org"`) must use a workspace app (`clientOwner: "org"`). A personal connection may use a shared workspace app.
</ParamField>

<ParamField body="owner" type="Owner" required>
Owner under which the minted connection is saved.
</ParamField>

<Tip>
Requested OAuth scopes come from the integration's declared auth template, not from the OAuth client row. Reusing a narrow client on a broad integration still requests the integration's full declared scope set.
</Tip>

OAuth-minted connections carry `oauthClient`, `oauthClientOwner`, `expiresAt`, and `oauthScope` on the connection row. Token refresh runs automatically inside `resolveConnectionValues` before each tool invocation when the access token is expired or near expiry.

Hosts must configure `redirectUri` on `createExecutor` (derived as `${webBaseUrl}${mountPrefix}/oauth/callback`). Redirect-requiring flows fail loudly when it is missing.

## SDK surface

`executor.connections` exposes:

| Method | Input | Returns |
| --- | --- | --- |
| `create` | `CreateConnectionInput` | `Connection` |
| `list` | `{ integration?, owner? }` | `Connection[]` |
| `get` | `ConnectionRef` | `Connection \| null` |
| `update` | `ConnectionRef`, `UpdateConnectionInput` | `Connection` |
| `remove` | `ConnectionRef` | `void` |
| `refresh` | `ConnectionRef` | `Tool[]` |

`UpdateConnectionInput` only accepts `description` and `identityLabel`. Credentials and OAuth lifecycle fields are not editable through update; recreate or refresh instead.

Plugins resolve credentials through `ctx.connections.resolveValue(ref)` (primary `token` variable) or `resolveValues(ref)` (full variable map).

## HTTP API

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/connections` | List connections (`?integration=`, `?owner=`) |
| `POST` | `/connections` | Create or replace a static connection |
| `GET` | `/connections/:owner/:integration/:name` | Get one connection |
| `PATCH` | `/connections/:owner/:integration/:name` | Update metadata |
| `DELETE` | `/connections/:owner/:integration/:name` | Remove connection and its tools |
| `POST` | `/connections/:owner/:integration/:name/refresh` | Re-resolve and persist tools |

:::endpoint POST /connections
Create or upsert a static (non-OAuth) connection. Payload must include exactly one of `value`, `values`, or `from`.
:::

:::endpoint GET /connections/:owner/:integration/:name
Fetch a single connection by its `(owner, integration, name)` key. Returns 404 `ConnectionNotFoundError` when absent.
:::

:::endpoint POST /connections/:owner/:integration/:name/refresh
Re-run the integration plugin's `resolveTools` for this connection and return the updated tool list.
:::

OAuth routes live under `/oauth/*` (`/oauth/start`, `/oauth/complete`, `/oauth/clients`, etc.) and mint connections through a separate path. See the HTTP API reference for full OAuth payloads.

## Errors

| Error | HTTP | When |
| --- | --- | --- |
| `IntegrationNotFoundError` | 404 | Integration slug does not exist |
| `ConnectionNotFoundError` | 404 | `(owner, integration, name)` not found |
| `InvalidConnectionInputError` | 400 | Empty credential on credentialed template, mixed origins, missing user subject, or multiple origin fields |
| `CredentialProviderNotRegisteredError` | 409 | Referenced provider is not registered, or no writable provider for pasted values |
| `OAuthStartError` | 400 | Missing client, workspace/personal app mismatch, missing `redirectUri` |
| `OAuthCompleteError` | 400 | Code exchange failed or session corrupt |
| `OAuthSessionNotFoundError` | 404 | Unknown or expired OAuth session state |

At invoke time, missing resolved values surface as `connection_value_missing` through the auth tool failure path.

## Security model

- Agents and sandboxes never receive raw credential values. Connections store routing (`provider` + `item_ids`); values resolve inside the executor at call time.
- OAuth client secrets and access tokens live in the credential provider, not in connection API responses.
- Removing a connection deletes its tool and definition rows. External provider items are left intact when the provider is read-only; writable providers may delete stored items on OAuth client removal.

## Related pages

<CardGroup>
<Card title="Integrations" href="/integrations">
Tenant-level catalog identities that connections bind to.
</Card>

<Card title="Configure credentials" href="/configure-credentials">
Credential providers, OAuth flows, and placement-based auth templates.
</Card>

<Card title="Tools" href="/tools">
How connections produce callable tool addresses.
</Card>

<Card title="SDK quickstart example" href="/sdk-quickstart-example">
End-to-end `connections.create` walkthrough with an in-memory provider.
</Card>

<Card title="HTTP API reference" href="/http-api-reference">
Full `/connections` and `/oauth` route payloads and error shapes.
</Card>
</CardGroup>
