---
title: "Workspace Connections"
description: "Shared provider metadata, grants, and credential refs for connect-once-use-everywhere integrations."
---

# Workspace Connections

<Callout tone="info">

**Developer page.** This page is for developers wiring an app to a shared, reusable provider connection. For the end-user catalog of connectable providers, see [Integrations](/docs/integrations).

</Callout>

Workspace connections are the framework primitive for reusable integration metadata. They make "connect once, grant apps, reuse credentials" possible without pretending every provider is fully generic.

## Quickstart {#quickstart}

### The four concepts

- **Connection** — a named provider account (`team-slack`, `acme-hubspot`). Records provider id, account label, status, scopes, and safe config. Never stores secret values.
- **Grant** — permission for a specific app to use a connection. An app without a grant cannot see the connection's credentials.
- **credentialRef** — a pointer to a vault secret (`{ key: "SLACK_BOT_TOKEN", scope: "org" }`). The connection says where the token lives; the vault holds the value.
- **Readiness** — the combined status an app sees: `connected` (granted + credentials present), `needs_grant`, `needs_credentials`, `needs_attention`, or `not_configured`.

<Diagram id="doc-block-1pszxki" title="Connect once, grant apps, reuse credentials" summary={"A Connection holds provider metadata (never secrets) and credentialRefs that point at the vault. Per-app Grants unlock it. Apps read a single Readiness status."}>

```html
<div class="diagram-conn">
  <div class="diagram-panel col" data-rough>
    <span class="diagram-pill accent">Connection</span>
    <div class="diagram-box" data-rough>
      named provider account<br /><small class="diagram-muted"
        >provider, label, status, scopes, config &middot; never stores secret
        values</small
      >
    </div>
    <div class="diagram-muted">
      credentialRef &rarr; pointer to a vault secret
    </div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel col" data-rough>
    <span class="diagram-pill">Grant</span>
    <div class="diagram-box" data-rough>
      per-app permission<br /><small class="diagram-muted"
        >no grant = no credential access</small
      >
    </div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel col" data-rough>
    <span class="diagram-pill ok">Readiness</span
    ><small class="diagram-muted">what the app sees</small>
    <div class="sev-row">
      <span class="diagram-pill ok">connected</span
      ><span class="diagram-pill warn">needs_grant</span>
    </div>
    <div class="sev-row">
      <span class="diagram-pill warn">needs_credentials</span
      ><span class="diagram-pill warn">needs_attention</span>
    </div>
    <div class="sev-row"><span class="diagram-pill">not_configured</span></div>
  </div>
</div>
```

```css
.diagram-conn {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.diagram-conn .col {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px;
  min-width: 220px;
}
.diagram-conn .diagram-arrow {
  font-size: 22px;
  line-height: 1;
}
.diagram-conn .sev-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
```

</Diagram>

### Worked example: Slack

Connect Slack once and grant it to Brain and Analytics:

```ts
import {
  upsertWorkspaceConnection,
  upsertWorkspaceConnectionGrant,
} from "@agent-native/core/workspace-connections";

await upsertWorkspaceConnection({
  id: "acme-slack",
  provider: "slack",
  label: "Acme Slack",
  accountId: "T012345",
  accountLabel: "Acme",
  status: "connected",
  scopes: ["channels:history", "groups:history", "chat:write"],
  config: {
    teamDomain: "acme",
    channelHints: ["product", "dev-fusion", "customer-success"],
  },
  credentialRefs: [{ key: "SLACK_BOT_TOKEN", scope: "org" }],
});

await upsertWorkspaceConnectionGrant({
  connectionId: "acme-slack",
  appId: "brain",
});
await upsertWorkspaceConnectionGrant({
  connectionId: "acme-slack",
  appId: "analytics",
});
```

<DataModel
  id="doc-block-1goc7zn"
  title="The connection model"
  summary={
    "A connection records safe provider metadata and credentialRefs (pointers, not secrets). Each grant unlocks one app — one connection, many grants."
  }
  entities={[
    {
      id: "conn",
      name: "workspace_connections",
      note: "Named provider account. Never stores secret values.",
      fields: [
        {
          name: "id",
          type: "string",
          pk: true,
          note: "e.g. acme-slack",
        },
        {
          name: "provider",
          type: "string",
          note: "stable provider id, e.g. slack",
        },
        {
          name: "label",
          type: "string",
        },
        {
          name: "accountId",
          type: "string",
          nullable: true,
        },
        {
          name: "accountLabel",
          type: "string",
          nullable: true,
        },
        {
          name: "status",
          type: "string",
          note: "e.g. connected",
        },
        {
          name: "scopes",
          type: "string[]",
          nullable: true,
        },
        {
          name: "config",
          type: "json",
          nullable: true,
          note: "safe, non-secret config",
        },
        {
          name: "credentialRefs",
          type: "json",
          nullable: true,
          note: "pointers to vault keys, e.g. { key, scope }",
        },
      ],
    },
    {
      id: "grant",
      name: "workspace_connection_grants",
      note: "Per-app permission to use a connection.",
      fields: [
        {
          name: "connectionId",
          type: "string",
          fk: "conn.id",
        },
        {
          name: "appId",
          type: "string",
          note: "e.g. brain, analytics",
        },
      ],
    },
  ]}
  relations={[
    {
      from: "conn",
      to: "grant",
      kind: "1-n",
      label: "grants apps",
    },
  ]}
/>

### What apps call

Before asking a user to paste a new key, check readiness first:

```ts
import { listWorkspaceConnectionProviderCatalogForApp } from "@agent-native/core/workspace-connections";

const catalog = await listWorkspaceConnectionProviderCatalogForApp({
  appId: "brain",
  templateUse: "brain",
  provider: "slack",
  includeConnections: "all",
});

const slack = catalog.providers[0];
if (slack.workspaceConnection.grantState === "needs_grant") {
  // Show "Grant Brain access" instead of asking for a second Slack token.
}
if (slack.readiness.status === "needs_credentials") {
  // Show the missing credential ref names, never a secret value.
}
```

## Reference {#reference}

### Provider Catalog

Import the catalog from `@agent-native/core/connections`:

```ts
import {
  getWorkspaceConnectionProvider,
  listWorkspaceConnectionProvidersForTemplate,
  workspaceConnectionProviderSupports,
} from "@agent-native/core/connections";

const brainProviders = listWorkspaceConnectionProvidersForTemplate("brain");
const slack = getWorkspaceConnectionProvider("slack");

if (workspaceConnectionProviderSupports("slack", "messages")) {
  // Offer a Slack source, sync check, or onboarding step.
}
```

The initial provider ids are:

| Provider       | Capabilities                   | Common uses                                 |
| -------------- | ------------------------------ | ------------------------------------------- |
| `slack`        | search, import, messages       | brain, dispatch, analytics                  |
| `github`       | search, import, code, docs     | brain, analytics, dispatch                  |
| `figma`        | search, import, docs           | brain, design, slides, content              |
| `notion`       | search, import, docs           | brain, content, dispatch                    |
| `gmail`        | search, import, messages       | mail, brain, dispatch                       |
| `google_drive` | search, import, docs           | brain, content, slides, dispatch, analytics |
| `hubspot`      | search, import, crm            | analytics, brain, crm, mail, dispatch       |
| `salesforce`   | search, import, crm            | analytics, brain, crm, dispatch             |
| `jira`         | search, import, code, docs     | analytics, brain, dispatch (Jira Cloud)     |
| `sentry`       | search, import, docs           | analytics, brain, dispatch                  |
| `granola`      | search, import, meetings, docs | brain, calendar, dispatch                   |
| `clips`        | search, import, meetings       | brain, clips                                |
| `generic`      | search, import, docs           | custom webhooks and file drops              |

Credential keys are names only, such as `SLACK_BOT_TOKEN` or `GITHUB_TOKEN`. Provider metadata must never include actual credential values.

### Connection Store API

```ts
import {
  listWorkspaceConnectionProviderCatalogForApp,
  listWorkspaceConnectionGrants,
  listWorkspaceConnections,
  summarizeWorkspaceConnectionProviderForApp,
  summarizeWorkspaceConnectionProviderReadiness,
  upsertWorkspaceConnection,
  upsertWorkspaceConnectionGrant,
  revokeWorkspaceConnectionGrant,
} from "@agent-native/core/workspace-connections";

const connections = await listWorkspaceConnections({ includeDisabled: true });
const grants = await listWorkspaceConnectionGrants({ appId: "brain" });

const appGrant = summarizeWorkspaceConnectionProviderForApp({
  providerId: "slack",
  appId: "brain",
  connections,
  grants,
});

const readiness = summarizeWorkspaceConnectionProviderReadiness({
  provider: slack!,
  appId: "brain",
  connections,
  grants,
});

const brainCatalog = await listWorkspaceConnectionProviderCatalogForApp({
  appId: "brain",
  templateUse: "brain",
});
```

The `credentialRefs` array points at vault keys; it is not credential storage. For example, `{ key: "SLACK_BOT_TOKEN", scope: "org" }` tells a granted app to look up the org-scoped vault secret named `SLACK_BOT_TOKEN` when it needs to call Slack. Connection-level refs describe the provider account; grant-level refs can narrow or override what a specific app should use.

Connection rows are scoped to the active org when one is present. Without an org, they are scoped to the authenticated user. Grant rows use the same scope.

**Legacy `allowedApps` field:** `allowedApps: []` means every app in the same scope may use the connection; `allowedApps: ["dispatch"]` grants access through the legacy field. Use explicit `workspace_connection_grants` rows for new setup — they make revocation, audit, and per-app readiness easier. `revokeWorkspaceConnectionGrant(connectionId, appId)` removes an explicit grant but does not change legacy `allowedApps`.

Use `summarizeWorkspaceConnectionProviderForApp()` and `summarizeWorkspaceConnectionProviderReadiness()` for app-facing status instead of hand-rolling grant checks. The shared summaries return `grantState`, `grantAvailability`, safe credential ref names, per-app connection rows, and readiness fields such as `readyConnectionCount` and `missingRequiredCredentialKeys`.

For new app setup screens, prefer `listWorkspaceConnectionProviderCatalogForApp()` as the higher-level boundary — it combines the provider catalog, scoped connections, explicit grants, per-app access summaries, and provider readiness into one safe shape.

### How this complements the vault

The credential vault answers: "Where is the secret stored, who can access it, and which apps are granted it?"

Workspace connection provider metadata answers: "Which provider is this, what can it do, what credential keys might it need, and which templates should offer it?"

<Diagram id="doc-block-jpomi8" title="Connection store vs. vault" summary={"The vault owns the secret value. The connection owns provider metadata plus credentialRefs (pointers). At execution time the app resolves the ref through a granted connection and reads the value from the vault."}>

```html
<div class="diagram-vault">
  <div class="diagram-panel col" data-rough>
    <span class="diagram-pill accent">Connection store</span>
    <div class="diagram-box" data-rough>
      provider account + metadata<br /><small class="diagram-muted"
        >status, scopes, config</small
      >
    </div>
    <div class="diagram-box" data-rough>
      credentialRef<br /><small class="diagram-muted"
        >{ key: SLACK_BOT_TOKEN, scope: org }</small
      >
    </div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-card">
    <span class="diagram-pill">App action</span
    ><small class="diagram-muted"
      >resolves at execution time through a granted ref</small
    >
    <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel col" data-rough>
    <span class="diagram-pill ok">Vault</span>
    <div class="diagram-box" data-rough>
      secret value<br /><small class="diagram-muted"
        >never returned to the agent or UI</small
      >
    </div>
  </div>
</div>
```

```css
.diagram-vault {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.diagram-vault .col {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px;
  min-width: 220px;
}
.diagram-vault .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px 14px;
}
.diagram-vault .diagram-arrow {
  font-size: 22px;
  line-height: 1;
}
```

</Diagram>

In practice, the vault secret gets created first (by Dispatch or another workspace setup flow), then the connection row and its grants point at it — the [four concepts](#the-four-concepts) above cover the rest of the lifecycle. The one thing worth calling out here: app-specific SQL (source ids, cursors, filters, sync windows, metric definitions, review rules, user choices) never moves into the connection store — that stays app-local even after a provider is shared. See [Credential resolution](#credential-resolution) below for exactly how an action turns a granted ref into a usable secret at execution time.

### Provider reader runtime

The provider-reader layer is a contract first, not a promise that every provider has a shared live reader. Reader definitions describe supported operations, credential requirements, and implementation status: `metadata-only`, `template-owned`, or `shared`. The runtime resolves the granted workspace connection and credential refs for an app, calls a registered handler, and returns normalized items without exposing secret values.

Most live handlers remain template-owned today, which means Brain still owns Slack/GitHub ingestion behavior and Analytics still owns analytics interpretation. Promote a reader to `shared` only when the provider-specific API calls, pagination, permissions, and result semantics are truly reusable across templates.

### App readiness pattern

Apps that consume shared provider credentials should expose a read-only readiness action and a small setup surface covering:

- **Provider catalog:** provider id, label, capabilities, recommended template uses, and required credential key names from `@agent-native/core/connections`.
- **Workspace summary:** connection count, active/granted counts, grant state, credential ref names, and non-secret account labels from `@agent-native/core/workspace-connections`.
- **Provider readiness:** `ready`, `needs_credentials`, `needs_attention`, `checking`, `disabled`, or `not_configured` via `summarizeWorkspaceConnectionProviderReadiness()`.
- **Source state:** app-local configured sources, cursors, sync status, and next action.

Brain's Sources page is the reference implementation. It shows reusable workspace connection providers beside Brain source records, labels grant states as `connected`, `granted`, `needs_grant`, or `not_connected`, and shows provider health as ready, missing keys, grant needed, needs repair, or metadata only.

### Building a reusable connector

When a new provider should work across multiple templates, add it at each of the three layers once: register the id/label/capabilities in `@agent-native/core/connections` (**Provider metadata**), store the connected account through `upsertWorkspaceConnection()` (the **Connection**), and let each consuming app keep only its own app-local source config (Slack channels, GitHub repositories, HubSpot object filters, sync cursors, polling cadence) — never a second copy of the OAuth/token storage. The connection record says "this is Acme Slack and its token lives at `SLACK_BOT_TOKEN`"; the app-local source says "Brain may ingest `#product` and `#dev-fusion` from that Slack connection."

### Dispatch control-plane setup

Dispatch exposes control-plane actions that write the same shared store functions an app could call directly — its `upsert-workspace-connection` action validates input against the provider catalog, then delegates to the same `upsertWorkspaceConnection()` shown throughout this page:

```ts
await upsertWorkspaceConnection({
  id: "team-slack",
  provider: "slack",
  label: "Acme Slack",
  accountId: "T012345",
  accountLabel: "acme",
  status: "connected",
  scopes: ["channels:history", "groups:history"],
  config: { teamDomain: "acme", preferredChannels: ["product", "dev-fusion"] },
  credentialRefs: [
    {
      key: "SLACK_BOT_TOKEN",
      scope: "org",
      provider: "slack",
      label: "Slack bot token",
    },
  ],
});

// Then grant the apps that should reuse the provider.
await upsertWorkspaceConnectionGrant({
  connectionId: "team-slack",
  appId: "brain",
});
await upsertWorkspaceConnectionGrant({
  connectionId: "team-slack",
  appId: "analytics",
});
```

Use `allowedApps: []` only when a connection should be available to every app in the same scope. Prefer explicit grant rows for production setup.

### Credential resolution

App execution code resolves credential values from granted `credentialRefs` through the vault in the active request scope. Brain's `source-credentials.ts` is the current reference implementation: it lists workspace connections for the provider, checks `getWorkspaceConnectionAppAccess` for `appId: "brain"`, merges connection-level and grant-level credential refs, and reads the first matching scoped vault secret. Other apps should follow that shape instead of reaching for `process.env`.

## Design notes {#design-notes}

<details>
<summary>Reader-promotion policy and path to "connect once, use everywhere"</summary>

### App-local boundary

Two rules follow from the [reusable-vs-app-local boundary](#building-a-reusable-connector) above:

- **Never fall back to deploy-level environment variables** for user/org source credentials in an app connector — env vars are global to the deployment and do not express workspace grants.
- **Agents check the catalog before asking for a new key.** If a user asks to connect Slack, GitHub, HubSpot, Gmail, Google Drive, Granola, or another shared provider, inspect the workspace connection catalog first: use it if `connected`, ask for or perform the app grant if `needs_grant`, ask for the missing vault key if `needs_credentials`. Only ask for a brand-new raw key when no reusable connection exists.

### Path to "connect once, use everywhere"

The provider catalog and grant store are the foundation for a broader workspace layer:

- Shared provider ids and capability names keep templates aligned.
- Workspace-level inventory can show which providers are configured across [Brain](/docs/template-brain), Mail, Analytics, [Dispatch](/docs/dispatch), and future apps.
- Connection rows record account labels, status, allowed apps, credential refs, and health checks without changing template-facing provider ids.
- Grant rows let a workspace owner connect once, then enable individual apps as the workspace adopts them.
- Agents can route work across apps knowing which providers are already connected and which apps have grants.
- Federated search can ask for providers with `search`, `docs`, `messages`, `meetings`, `crm`, or `code` capabilities instead of hardcoding every app's connector list.
- Provider-specific readers, OAuth refresh flows, ingestion checkpoints, and app-owned data models can become shared later, but they are not implied by a workspace connection today.

Keep the boundary strict: provider metadata is safe to show; credential values stay in the [vault](/docs/security).

</details>

## What's next {#whats-next}

- [**Dispatch**](/docs/dispatch) — the app that typically owns workspace connection setup and the vault
- [**Security**](/docs/security) — the credential vault, secret scoping, and access model connections point at
- [**Onboarding**](/docs/onboarding#checking-workspace-connections-in-onboarding) — checking connection readiness from a template's setup checklist
- [**MCP Clients**](/docs/mcp-clients) — sharing MCP server credentials across a workspace, a sibling reusable-config pattern
- [**Brain**](/docs/template-brain) — the reference implementation of a provider Sources page built on this model
