defineConnectionProvider({ type: 'oauth', ... }) won’t need to migrate.
defineConnectionProvider
Declare how your app's connections are obtained
defineConnectionProvider
Declare how your app's connections are obtained
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.nameis the unique identifier string used inlistConnections({ providerName })(kebab-case, must match^[a-z][a-z0-9-]*$).displayNameshows in the app’s General tab and in the AI tool list.scopedecides 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.logois a path to an asset bundled in your app’spublic/folder, served through the app’s public assets. When omitted, the General tab falls back to a placeholder built fromdisplayName.clientIdVariable/clientSecretVariableare names, not values — they must match keys declared indefineApplication.serverVariables. The actualclient_idandclient_secretare entered by the server admin through the app registration UI, never committed to your repo.- Use
serverVariables(notapplicationVariables) — OAuth credentials are server-wide and one OAuth app per Twenty server. - Until both
serverVariablesare 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 alongsideoauth.
Identify the connected account
Stop a reconnect from silently switching upstream accounts
Identify the connected account
Stop a reconnect from silently switching upstream accounts
identifyAccountLogicFunction to a logic function in the same app that returns a stable id for the account behind the freshly issued token: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.externalAccountIdmust 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 OIDCid_tokenemail, 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 barenullor 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.
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: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.Run a logic function on connect
React the moment a connection is established
Run a logic function on connect
React the moment a connection is established
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.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.Run a logic function on disconnect
Clean up when a connection is removed
Run a logic function on disconnect
Clean up when a connection is removed
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.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, 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.listConnections / getConnection
Use connections from a logic function
listConnections / getConnection
Use connections from a logic function
listConnections({ providerName }) returns this app’s ConnectedAccount rows for the given provider, with refreshed access tokens.- 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
authFailedAtset). getConnection(id)is the single-row equivalent.
reportConnectionAuthFailure
Tell the platform a credential is dead
reportConnectionAuthFailure
Tell the platform a credential is dead
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: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" }.Workspace or personal connections
Choose who connects with the provider scope
Workspace or personal connections
Choose who connects with the provider scope
- 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 withisAuthRequired: true) sees it; cron triggers and database events do not.
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 wasUSER) 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 aUSERprovider has no Add connection button, and the app’s settings only list the connections a member can already see.
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: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.One-time provider setup
Register your OAuth app with the third-party service
One-time provider setup
Register your OAuth app with the third-party service
- Go to the provider’s developer settings (e.g. https://linear.app/settings/api/applications/new).
- Set the Redirect URI to
<SERVER_URL>/auth/apps/callback. - Copy the generated Client ID and Client Secret.
- Open the installed app in Twenty as a server admin → set the values on the corresponding
serverVariables. - Workspace members can then add connections to
WORKSPACEproviders from the per-app Connections section.