> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connections

> Let your app act on a user's behalf in third-party services via OAuth.

Connections are credentials a user holds for an external service (Linear, GitHub, Slack, ...). Your app declares **how** those credentials are obtained — a **connection provider** — and consumes them at runtime to make authenticated calls to the third-party API.

Today only OAuth 2.0 is supported. Future credential types (personal access tokens, API keys, basic auth) will plug into the same surface — apps already using `defineConnectionProvider({ type: 'oauth', ... })` won't need to migrate.

<AccordionGroup>
  <Accordion title="defineConnectionProvider" description="Declare how your app's connections are obtained">
    A connection provider describes the OAuth handshake your app needs. For a `WORKSPACE` provider (the default), a member clicks "Add connection" in your app's settings, completes the provider's consent screen, and a `ConnectedAccount` row is created in their workspace. See "Workspace or personal connections" for `USER` providers.

    A working setup needs **two files** — the connection provider, and a matching `serverVariables` declaration on `defineApplication` that holds the OAuth client credentials.

    ```ts src/connection-providers/linear-connection.ts theme={null}
    import { defineConnectionProvider } from 'twenty-sdk/define';

    export default defineConnectionProvider({
      universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
      name: 'linear',
      displayName: 'Linear',
      icon: 'IconBrandLinear',
      // Optional: a logo bundled in your app's public/ folder, shown next to the
      // provider in the app's General tab.
      logo: 'public/linear-logomark.svg',
      type: 'oauth',
      scope: 'WORKSPACE',
      oauth: {
        authorizationEndpoint: 'https://linear.app/oauth/authorize',
        tokenEndpoint: 'https://api.linear.app/oauth/token',
        scopes: ['read', 'write'],
        // These must match keys in `defineApplication.serverVariables` below.
        clientIdVariable: 'LINEAR_CLIENT_ID',
        clientSecretVariable: 'LINEAR_CLIENT_SECRET',
        // Optional: defaults to 'json'. Some providers (Linear, Slack) want
        // 'form-urlencoded' for the token request.
        tokenRequestContentType: 'form-urlencoded',
        // Optional: defaults to true. Disable only if the provider rejects PKCE.
        usePkce: false,
        // Optional: extra query params on the authorize URL.
        // authorizationParams: { prompt: 'consent' },
        // Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
        // revokeEndpoint: 'https://example.com/oauth/revoke',
      },
      // Optional: a logic function in this app that returns a stable id for the
      // upstream account, run before the tokens are saved. See "Identify the
      // connected account".
      // identifyAccountLogicFunction: { universalIdentifier: '2f1e0d9c-...-...' },
      // Optional: a logic function in this app to run right after a connection is
      // established. See "Run a logic function on connect".
      // onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
      // Optional: a logic function in this app to run right after a connection is
      // removed. See "Run a logic function on disconnect".
      // onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
    });
    ```

    ```ts src/application.config.ts theme={null}
    import { defineApplication } from 'twenty-sdk/define';

    export default defineApplication({
      universalIdentifier: '...',
      displayName: 'Linear',
      description: 'Connect Linear to Twenty.',
      // OAuth client credentials live on the app registration (one OAuth app per
      // Twenty server, configured by the admin) — not per-workspace. Declare them
      // as serverVariables so the admin can fill them in once for all installs.
      serverVariables: {
        LINEAR_CLIENT_ID: {
          description: 'OAuth client ID from your Linear OAuth application.',
          isSecret: false,
          isRequired: true,
        },
        LINEAR_CLIENT_SECRET: {
          description: 'OAuth client secret from your Linear OAuth application.',
          isSecret: true,
          isRequired: true,
        },
      },
    });
    ```

    Key points:

    * `name` is the unique identifier string used in `listConnections({ providerName })` (kebab-case, must match `^[a-z][a-z0-9-]*$`).
    * `displayName` shows in the app's General tab and in the AI tool list.
    * `scope` decides who connects: `'WORKSPACE'` (the default) for connections shared with the whole workspace, `'USER'` for connections each member makes for themselves. See "Workspace or personal connections" below.
    * `logo` is a path to an asset bundled in your app's `public/` folder, served through the app's public assets. When omitted, the General tab falls back to a placeholder built from `displayName`.
    * `clientIdVariable` / `clientSecretVariable` are **names**, not values — they must match keys declared in `defineApplication.serverVariables`. The actual `client_id` and `client_secret` are entered by the server admin through the app registration UI, never committed to your repo.
    * Use `serverVariables` (not `applicationVariables`) — OAuth credentials are server-wide and one OAuth app per Twenty server.
    * Until both `serverVariables` are filled in, the app's General tab shows a "needs server admin" hint and the "Add connection" button is disabled.
    * `type: 'oauth'` is the only supported value today. The discriminator is forward-compatible: future types (`'pat'`, `'api-key'`, ...) will add new sub-config blocks alongside `oauth`.

    The OAuth callback URL your provider needs to whitelist is:

    ```
    https://<your-twenty-server>/auth/apps/callback
    ```
  </Accordion>

  <Accordion title="Identify the connected account" description="Stop a reconnect from silently switching upstream accounts">
    When a user clicks **Reconnect**, nothing stops them from authorizing a different upstream account than the one the connection was made with: Slack workspace B instead of workspace A, another GitHub organization, another Google account. Without a way to tell the two apart, Twenty would overwrite A's tokens with B's and everything your app built on that connection would quietly start talking to B.

    Set `identifyAccountLogicFunction` to a logic function in the same app that returns a stable id for the account behind the freshly issued token:

    ```ts src/connection-providers/slack-connection.ts theme={null}
    import { defineConnectionProvider } from 'twenty-sdk/define';

    import { SLACK_IDENTIFY_ACCOUNT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';

    export default defineConnectionProvider({
      universalIdentifier: '...',
      name: 'slack',
      displayName: 'Slack',
      type: 'oauth',
      oauth: {
        /* ... */
      },
      identifyAccountLogicFunction: {
        universalIdentifier: SLACK_IDENTIFY_ACCOUNT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
      },
    });
    ```

    **When it runs.** Synchronously inside the OAuth callback, right after the authorization code is exchanged for tokens and **before** anything is saved. The user is waiting on the redirect, so when the app is installed or synced Twenty caps the function's `timeoutSeconds` at 10, whatever the manifest declares; keep it to a single request to the provider's "who am I" endpoint. The on-connect hook, if any, still runs afterwards, once the connection is saved.

    **Payload.** The handler receives the provisional access token. It has not been persisted yet, so `getConnection` cannot return it; use it directly and do not log it.

    ```ts theme={null}
    type IdentifyAccountPayload = {
      connectionProviderId: string;
      connectionProviderName: string; // e.g. 'slack'
      accessToken: string;
    };
    ```

    **Result.** Return the account's id, and optionally a handle to display:

    ```ts theme={null}
    type IdentifyAccountResult = {
      externalAccountId: string | null; // non-empty, at most 512 characters; null opts out
      handle?: string; // non-empty when present
    };
    ```

    * `externalAccountId` must be stable for the life of the upstream account (a team id, an organization id, a user id), never a name the user can change. Twenty trims it and stores it on the connection on first connect.
    * `handle`, when returned, replaces the handle Twenty would otherwise derive (the OIDC `id_token` email, or the connecting user's email). Return something a user recognizes, such as the Slack workspace name.
    * Return `{ externalAccountId: null }` when the provider has no stable id to offer. Twenty then behaves as if no identify function were declared: no id is stored, a reconnect clears any id stored earlier, and reconnects are not checked. A bare `null` or a missing return is treated as an error, so a handler that forgets to return cannot silently disable the check.
    * If the function throws, times out, or returns anything else, the connection is **not** saved and the user lands back on the settings page with an error. Throw only when you genuinely cannot identify the account.

    **Reconnect refusal.** On a reconnect, Twenty compares the returned `externalAccountId` with the one stored on the connection. If they differ, the callback is refused before anything is written: the existing tokens, handle and scope stay as they were, the on-connect hook does not run, and the user sees which account they authorized and which one the connection belongs to. Connections created before your app declared the function have no stored id yet; their next reconnect stores it.

    Twenty does not deduplicate on `externalAccountId`: connecting the same upstream account twice through **Add connection** creates two connections. The id is exposed on `listConnections` and `getConnection`, so an app that wants one connection per upstream account can detect duplicates itself (for instance in its on-connect hook).

    **Writing the function.** Most providers expose a single endpoint that describes the current token. For Slack that is `auth.test`, which returns the workspace (`team_id`, `team`) and the user behind the token:

    ```ts src/logic-functions/slack-identify-account.ts theme={null}
    import { defineLogicFunction } from 'twenty-sdk/define';
    import type {
      IdentifyAccountPayload,
      IdentifyAccountResult,
    } from 'twenty-sdk/logic-function';

    import { SLACK_IDENTIFY_ACCOUNT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';

    export default defineLogicFunction({
      universalIdentifier: SLACK_IDENTIFY_ACCOUNT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
      name: 'slack-identify-account',
      timeoutSeconds: 10,
      handler: async ({
        accessToken,
      }: IdentifyAccountPayload): Promise<IdentifyAccountResult> => {
        const response = await fetch('https://slack.com/api/auth.test', {
          method: 'POST',
          headers: { Authorization: `Bearer ${accessToken}` },
        });
        const { ok, error, team_id, team } = await response.json();

        if (!ok) {
          throw new Error(`auth.test failed: ${error}`);
        }

        return { externalAccountId: team_id, handle: team };
      },
    });
    ```

    Other providers follow the same pattern, whether their "who am I" endpoint is REST or GraphQL. Throw when the provider rejects the token, and return the id as a string: convert numeric ids, such as GitHub user ids, with `String(id)`.

    Identification ships with Twenty 2.48.0. If your app relies on it, declare `"engines": { "twenty": ">=2.48.0" }` in its `package.json`.
  </Accordion>

  <Accordion title="Run a logic function on connect" description="React the moment a connection is established">
    Some providers hand you data at connect time that you need to persist before the connection is usable — the classic example is Slack, where the OAuth response identifies the workspace's `team_id` that inbound events will be keyed by. Set `onConnectLogicFunction` to reference a logic function in the same app (by its `universalIdentifier`), and it runs right after the `ConnectedAccount` is created.

    ```ts src/connection-providers/slack-connection.ts theme={null}
    export default defineConnectionProvider({
      universalIdentifier: '...',
      name: 'slack',
      displayName: 'Slack',
      type: 'oauth',
      oauth: {
        /* ... */
      },
      // Runs claimSlackTeam after every successful Slack connection.
      onConnectLogicFunction: {
        universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
      },
    });
    ```

    The hook runs **asynchronously in the connecting workspace** (it is enqueued, not awaited), so a slow or failing hook never blocks or breaks the OAuth callback — make it idempotent and handle its own retries. The handler receives:

    ```ts theme={null}
    type OnConnectPayload = {
      connectionProviderId: string;
      connectionProviderName: string; // e.g. 'slack'
      connectedAccountId: string;
    };
    ```

    From there use `getConnection(connectedAccountId)` to read the fresh access token and call the provider's API (e.g. Slack `auth.test`) or persist a mapping with the [key-value store](/developers/extend/apps/logic/key-value-store).
  </Accordion>

  <Accordion title="Run a logic function on disconnect" description="Clean up when a connection is removed">
    Anything an app claims at connect time has to be released when the connection goes away. A Slack integration that claims a `team_id` on connect, for instance, has to release that claim so another workspace can connect the same Slack team. Set `onDisconnectLogicFunction` to reference a logic function in the same app, and it runs right before the `ConnectedAccount` and its token are deleted, so the handler can still call the provider with `getConnection` to release anything the on-connect hook set up remotely.

    ```ts src/connection-providers/slack-connection.ts theme={null}
    export default defineConnectionProvider({
      universalIdentifier: '...',
      name: 'slack',
      displayName: 'Slack',
      type: 'oauth',
      oauth: {
        /* ... */
      },
      // Runs releaseSlackTeam after every Slack disconnection.
      onDisconnectLogicFunction: {
        universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
      },
    });
    ```

    Unlike the on-connect hook it runs **inline in the disconnecting workspace**, once, without retries: a failure is captured and the disconnect still completes. The handler receives the same payload shape:

    ```ts theme={null}
    type OnDisconnectPayload = {
      connectionProviderId: string;
      connectionProviderName: string; // e.g. 'slack'
      connectedAccountId: string;
    };
    ```

    The `ConnectedAccount` and its token are still present when the hook runs, so `getConnection(connectedAccountId)` resolves with a fresh access token and the handler can delete whatever it registered remotely. State the provider cannot give back (a `team_id`, an external subscription id) still belongs in the [key-value store](/developers/extend/apps/logic/key-value-store), written at connect time and keyed by `connectedAccountId`. Keep the handler short: it runs inside the disconnect request, so its `timeoutSeconds` is the upper bound on how long that request can take.

    The hook fires when a connection is removed on its own. Uninstalling the app drops its connections through a database cascade instead, so the hook does not run there. Declare an `uninstallLogicFunction` on `defineApplication` for that path: it runs before the app's metadata is deleted, so it can still call `listConnections` and clean up whatever is left.
  </Accordion>

  <Accordion title="listConnections / getConnection" description="Use connections from a logic function">
    Inside a logic function handler, `listConnections({ providerName })` returns this app's `ConnectedAccount` rows for the given provider, with refreshed access tokens.

    ```ts src/logic-functions/handlers/create-linear-issue-handler.ts theme={null}
    import { listConnections } from 'twenty-sdk/logic-function';

    export const createLinearIssueHandler = async (input: {
      teamId?: string;
      title?: string;
    }) => {
      if (!input.teamId || !input.title) {
        return { success: false, error: 'teamId and title are required' };
      }

      const connections = await listConnections({ providerName: 'linear' });

      // Workspace-shared credentials win when present; fall back to the first
      // personal one. For HTTP-route triggers you typically pick the
      // request user's connection via event.userWorkspaceId instead.
      const connection =
        connections.find((c) => c.scope === 'WORKSPACE') ?? connections[0];

      if (!connection) {
        return {
          success: false,
          error:
            'Linear is not connected. Open the app settings and click "Add connection".',
        };
      }

      // Use connection.accessToken to call the third-party API.
      const response = await fetch('https://api.linear.app/graphql', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${connection.accessToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
        }),
      });

      return { success: response.ok };
    };
    ```

    Each connection has:

    | Field | Description |
    | - | - |
    | `id` | Unique row id; pass to `getConnection(id)` to refetch a single one |
    | `scope` | `'USER'` (private to one workspace member) or `'WORKSPACE'` (shared with all members) |
    | `visibility` | Deprecated: `scope` in lowercase (`'user'` or `'workspace'`). Use `scope` instead |
    | `scopes` | OAuth permissions granted by the upstream provider (distinct from `scope` — those are unrelated) |
    | `userWorkspaceId` | The owner's userWorkspace id — useful for picking "the request user's connection" in HTTP-route triggers |
    | `workspaceMemberId` | The owner's workspace member id, or `null` if they've since left the workspace |
    | `accessToken` | Fresh OAuth access token (refreshed automatically if expired) |
    | `name` | The connection's display name (auto-derived at OAuth callback, user-renameable) |
    | `handle` | The connected upstream account's label, refreshed on each reconnect: the `handle` returned by the identify function when there is one, otherwise the email from the OIDC `id_token` returned at token exchange, falling back to the connecting Twenty user's email |
    | `externalAccountId` | Stable id of the upstream account returned by the identify function, or `null` when the provider declares none or the connection predates it. Not unique: use it to deduplicate connections yourself |
    | `authFailedAt` | Set when the most recent refresh failed, or when the app reported the credential dead via `reportConnectionAuthFailure`; the user must reconnect |
    | `authFailedReason` | Human-readable explanation for `authFailedAt`, when the reporter gave one; shown on the connection detail page in settings |

    Key points:

    * Pass `{ providerName }` to filter by provider; omit it to get all connections this app owns across all providers.
    * The server transparently refreshes the access token before returning. Your handler always sees a usable token (or `authFailedAt` set).
    * `getConnection(id)` is the single-row equivalent.
  </Accordion>

  <Accordion title="reportConnectionAuthFailure" description="Tell the platform a credential is dead">
    The platform sets `authFailedAt` on its own only when an OAuth refresh fails. Providers whose tokens are never refreshed (a Slack bot token, for example) fail only at call time inside your handlers, so the connection row in settings would keep showing **Connected** while every call dies. When your code hits a definitive auth rejection, report it:

    ```ts theme={null}
    import { reportConnectionAuthFailure } from 'twenty-sdk/logic-function';

    await reportConnectionAuthFailure({
      connectionId: connection.id,
      reason:
        'Slack rejected the stored token (invalid_auth). Reconnect to restore the integration.',
    });
    ```

    This sets `authFailedAt` and the optional reason on the connection. The settings row flips to **Reconnect needed** with a Reconnect button, the connection detail page shows the reason, and `getConnection` starts throwing `AppConnectionAuthFailedError` for that row. The flag and reason clear automatically when the user reconnects. A `reason` longer than 1000 characters is rejected, so truncate provider payloads before passing them.

    Only report failures that a reconnect would actually fix (an `invalid_auth`-class rejection from the provider), never transient network errors. An app can only report its own connections, and a request user can only report their own personal credentials.

    The mutation behind this helper ships with Twenty 2.40.0, so calling it against an older server fails. If your app uses it, declare the dependency in your `package.json`: `"engines": { "twenty": ">=2.40.0" }`.
  </Accordion>

  <Accordion title="Workspace or personal connections" description="Choose who connects with the provider scope">
    Each connection has one of two scopes:

    * **Workspace shared** (`'WORKSPACE'`) — any workspace member can use the credential. Cron / database triggers also see it, since they have no request user.
    * **Just for me** (`'USER'`) — the credential is private to the connecting member. Any logic function called on their behalf (HTTP-route trigger with `isAuthRequired: true`) sees it; cron triggers and database events do not.

    The provider's `scope` decides which one its new connections get:

    * `scope: 'WORKSPACE'` (the default) — the app works with connections shared with the workspace. **Add connection** in the app's settings creates a workspace-shared connection, and a member can turn a personal connection made earlier (before Twenty 2.49.0, or while the provider was `USER`) into a shared one with **Share with workspace**.
    * `scope: 'USER'` — each member connects their own account, creating personal connections. Members will connect from their **App preferences**, which is not available yet: until then a `USER` provider has no **Add connection** button, and the app's settings only list the connections a member can already see.

    A provider has exactly one scope. If your app needs both a shared credential and one per member for the same service (a bot token and each member's own account, say), declare two providers.

    Twenty refuses a new connection whose scope does not match the provider's, including when your app changes its scope while a member is on the provider's consent screen. Changing `scope` in a later version does not move or delete existing connections: they keep their scope and can still be disconnected or reconnected.

    Adding or reconnecting a **Just for me** connection requires `CONNECTED_ACCOUNTS` permission, and adding or reconnecting a **Workspace shared** one requires `APPLICATIONS` permission. Reconnecting a **Workspace shared** connection as **Just for me** requires both.

    Use the right connection for each handler:

    ```ts theme={null}
    // HTTP-route trigger — prefer the request user's own connection.
    const conn =
      connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
      connections.find((c) => c.scope === 'WORKSPACE');

    // Cron trigger — no request user; only shared credentials are sensible.
    const conn = connections.find((c) => c.scope === 'WORKSPACE');
    ```

    Multiple connections per (user, provider) are allowed, so the same user can hold "Personal Linear" and "Work Linear" side by side.

    Provider scope ships with Twenty 2.49.0. If your app declares `scope`, declare `"engines": { "twenty": ">=2.49.0" }` in its `package.json`. In the same version a connection's `scope` replaces its `visibility`, which stays available as a deprecated lowercase alias. `listConnections` and `getConnection` from `twenty-sdk` 2.49.0 read `scope`, so apps built with it need a 2.49.0 server.
  </Accordion>

  <Accordion title="One-time provider setup" description="Register your OAuth app with the third-party service">
    For each connection provider, the server admin needs to register an OAuth app at the third party first.

    1. Go to the provider's developer settings (e.g. [https://linear.app/settings/api/applications/new](https://linear.app/settings/api/applications/new)).
    2. Set the **Redirect URI** to `<SERVER_URL>/auth/apps/callback`.
    3. Copy the generated **Client ID** and **Client Secret**.
    4. Open the installed app in Twenty as a server admin → set the values on the corresponding `serverVariables`.
    5. Workspace members can then add connections to `WORKSPACE` providers from the per-app **Connections** section.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.