Skip to content
Kortix
Esc
↑↓navigate↵open⌘Jpreview
On this page

Connectors

Give agents and your own code governed access to external apps, MCP servers and APIs: Kortix holds the credential, applies your policies and records every call.

A connector gives a project governed access to one external app, MCP server or API. Agents call it as a tool, and your own code calls it through the SDK. Kortix stores the credential, picks the account, applies the project’s policies, calls the provider and writes an audit event for every call. The credential never enters a sandbox.

This page is for project members who connect apps, decide who may use them, and call them from sessions or code.

What a connector is

A connector has:

  • a slug, unique in the project ([a-z0-9][a-z0-9_-], up to 128 characters);
  • a display name;
  • a provider, which decides how Kortix reaches the service;
  • its actions, the tools an agent calls (<slug>.<action>);
  • its policies, which decide which actions run, need approval, or are blocked.

A connector holds one or more accounts. An account is the authorized identity a call runs as.

Connectors live in kortix.yaml under connectors:, or in a file it lists under imports:. The manifest never holds a credential.

Provider Use it for Required fields
composio The managed catalog of SaaS apps (Gmail, GitHub, Linear, …). The default for managed apps. app (the toolkit slug)
mcp A remote MCP server. url; transport is http (default) or sse
openapi An API with an OpenAPI document. spec (a URL or a repo path)
postman An API described by a Postman collection. spec
graphql A GraphQL endpoint. Kortix introspects it when spec is absent. endpoint
http A raw HTTP API. base_url
channel Slack, Microsoft Teams or email. Kortix writes it when you connect a channel. platform
computer Built in. Each connected computer is one account. It is never written to kortix.yaml. —

pipedream is a legacy provider: existing connectors run, and new managed apps use composio. With connectors_v2 on, move a Pipedream or Composio connector to Kortix.

A provider: mcp connector is Kortix calling someone else’s MCP server. The Kortix MCP server is the other direction: your MCP client calls Kortix.

Add a connector

From the web app

  1. Open Customize → Connectors.
  2. On the All tab, find the app and select Install.
  3. When the app has its own hosted MCP server with one-click sign-in, Connect app opens. That means OAuth with dynamic client registration, for example Linear, Notion or Sentry. Choose Who can use it (Only you by default) and select Connect. You sign in to the app and return to the connector’s page with its tools loaded.
  4. Otherwise Kortix adds the managed app, opens its page and opens Add account with Only you selected. Authorize an account there.
  • Install shows only to members who can change the project’s connectors. Other members add their own account on an installed connector’s Accounts tab.
  • Installing an app that the project already has adds an account to the existing connector. It does not create a second connector.
  • An app page lists its Ways to connect (for example an MCP server and a REST API). Install uses the MCP server when the catalog lists one.
  • Kortix checks the app’s MCP server when you select Install. A server that needs a pasted token, or has no dynamic client registration (for example GitHub’s remote MCP server), installs as the managed app instead.
  • For an MCP server, add it from its URL. For another API that is not in the catalog, select New → Add a custom connector and choose OpenAPI, Postman, GraphQL, MCP or HTTP. Paste the source URL and Kortix proposes the slug: the document’s title, or else the URL’s domain. Kortix then opens the connector’s page. An MCP server that asks for sign-in (HTTP 401 or 403) opens Add account there, as Install does. This needs an auth scheme: one you pick under Auth, or one Auto-detect finds in the server’s WWW-Authenticate challenge. Without one, the 401 or 403 is shown as the failure. New → Create in chat has an agent scaffold the connector and open a change request.
  • When Kortix cannot load a new connector’s tools, the connector’s page shows the reason and Retry. The connector stays in the project. OAuth 2.0 client credentials entered in the form are saved as well, so Retry uses them.
  • With OAuth 2.0 client credentials, Added and connected means the tools loaded with them. When the server still refuses them, the page shows the refusal and Retry.
  • A search on All also covers published OpenAPI, MCP, GraphQL and Postman surfaces when the project flag Connectors API Discover (connectors_api_discover, off by default) is on. Turn it on under Customize → Settings → Feature flags.

A connector page has up to four tabs:

Tab Shows Who sees it
Accounts The accounts the connector can run as. Everyone with access to the project.
Tools Each action, and whether it runs, needs approval, or is blocked. Members who can change connectors.
Triggers App event triggers on this connector. Everyone, when the app has events.
Settings Connection details and Remove connector. Members who can change connectors. Absent on the computer connector.

Add an MCP server from its URL

  1. Open Customize → Connectors and select New → MCP server.
  2. Paste the server’s URL, for example https://mcp.linear.app/mcp. Kortix names the connector after the domain (linear) and shows the name under the field.
  3. Choose Who can use it. Only you is selected by default.
  4. Select Connect. Kortix adds the connector and the account, then opens the server’s sign-in. After you approve, you return to the connector’s page with its tools loaded.
  • Only you adds no shared account. Your login stays in your private account.
  • Everyone in project and Specific people, groups, or agents need permission to manage the project’s connections.
  • A server that needs no sign-in (for example DeepWiki) is ready when the account exists.
  • A server that asks for a credential but has no one-click OAuth opens credential entry on the connector page.
  • A sign-in that cannot start shows the server’s reason. The connector and the account stay; select Sign in on the connector page to try again.

The MCP app catalog (experimental)

With the project flag Connectors v2 (connectors_v2, off by default) on, the All tab lists the Kortix MCP app catalog instead of the managed apps. Every app in it is a remote MCP server that its vendor runs, and Kortix connects to it directly. Turn the flag on under Customize → Settings → Feature flags.

  1. Find the app and select Install.
  2. Connect app opens. Choose Who can use it and select Connect.
  3. You sign in to the app and return to the connector’s page with its tools loaded.

Apps sign in one of three ways:

Sign-in Apps Setup
The vendor registers Kortix on the fly Linear, Notion, Atlassian, Figma, Stripe, Sentry, Airtable, Dropbox, Canva, Miro, monday.com, ClickUp, Intercom, PostHog, Supabase, Vercel, Cloudflare and more None
Kortix’s own OAuth client Gmail, Google Calendar, Google Drive, Google Docs, Google Sheets, GitHub, Slack, HubSpot, Asana, Box The operator configures the client (KORTIX_OAUTH_CLIENTS, see Kortix OAuth clients). Until then the app is not listed.
None DeepWiki, Context7 None. The app is ready when it is installed.
  • Kortix’s own OAuth client is used only for the exact MCP server URL in the catalog, with the catalog’s endpoints and scopes. Its client secret stays on the server.
  • Connectors you added from the managed catalog keep working with the flag on.
  • Agents see the same catalog: the search_connector_apps tool of the Kortix MCP server lists the MCP apps, and add_connector with app adds the app’s MCP server.

Catalog apps can start triggers. With the project flag event_triggers also on, Kortix checks an installed app’s MCP server for new items, such as a new email in Gmail, and fires the trigger once for each one. Every tool that lists items and only reads can be an event, on catalog apps and on your own MCP servers. The trigger runs on an account shared with the whole project.

Move a Pipedream or Composio connector to Kortix

Kortix retires Pipedream and Composio. With connectors_v2 on, a Pipedream or Composio connector moves to the catalog app for the same service and keeps its slug. Their sign-ins belong to their own OAuth clients, so they do not carry over: one person signs in to the app once, and the connector switches.

What stays:

  • The slug. Agent grants (agents.<name>.connectors), session bindings and every other reference to the slug keep working.
  • The display name, the entry’s sensitive flag, and a * rule in its policies. A rule that names a tool (for example send_email on the connector, or gmail.send_email in the project’s rules) names the old provider’s tool, and the Kortix app names its tools differently: a connector with such a rule does not move until you remove it. Add rules for the new tool names after the move.
  • Every account row, with its name, owner and audience.
  • The old provider serves the connector until the sign-in completes.

What changes:

  • Kortix rewrites the kortix.yaml entry to provider: mcp with the app’s url, commits it to the default branch and syncs the connector.
  • The tools now come from the vendor’s own MCP server, so the tool names differ. Check the agent prompts that name an old tool.
  • The account that signed in holds the new sign-in. Every other account on the connector loses its old sign-in. It reads not signed in until its owner signs in again.
  • A Composio event trigger on the connector moves to the Kortix event that replaces it, in the same kortix.yaml commit, and its Composio subscription is deleted. Kortix then checks the app itself.
# Before
connectors:
  - slug: issues
    name: Issues
    provider: composio
    app: linear

# After
connectors:
  - slug: issues
    name: Issues
    provider: mcp
    url: https://mcp.linear.app/mcp
    transport: http

Composio event triggers that move:

Composio event Kortix event Config
GMAIL_NEW_GMAIL_MESSAGE gmail.message.received query becomes in:inbox (<query>), so the trigger stays inbox-only. labelIds must be empty or INBOX.
GOOGLECALENDAR_GOOGLE_CALENDAR_EVENT_CREATED_TRIGGER google_calendar.event.created calendarId becomes calendar_id. lookahead_days is 365, so an event created far ahead still fires. showDeleted is dropped.
GOOGLEDRIVE_FILE_CREATED_TRIGGER google_drive.file.created folder_id and query stay; a folder_id that is not a Drive folder id blocks the move. max_results is dropped.

interval (minutes; Composio checks every minute when it is unset) rounds up to the next Kortix value: 1, 5, 15, 30, 60, 360 or 1440. A trigger in an imported file is rewritten in that file. A connector with any other Composio event trigger does not move until you delete or change that trigger.

Which apps move:

  • Each app in the table below moves to its catalog app.
  • Gmail, Google Calendar, Google Drive, Google Docs, Google Sheets, GitHub, HubSpot, Asana and Box move only when the deployment has a Kortix OAuth client for the vendor. See Kortix OAuth clients.
  • Slack does not move: connect the Slack channel instead, which replaces a Slack connector.
  • Any other app has no Kortix equivalent yet, for example jira_data_center, slack_bot, neon_postgres, googlesuper or supabase_read_mcp. The connector keeps working through its provider.
Kortix app Pipedream app Composio app
Airtable airtable_oauth airtable
Asana asana asana
Atlassian jira, confluence jira, confluence, atlassian_mcp
Attio attio attio
Box box box, box_mcp
Canva canva canva, canva_mcp
ClickUp clickup clickup, clickup_mcp
Close close close, close_mcp
Cloudflare cloudflare_api_key cloudflare, cloudflare_api_key, cloudflare_mcp
Dropbox dropbox dropbox
Figma figma figma
GitHub github github, github_mcp
Gmail gmail gmail
Google Calendar google_calendar googlecalendar
Google Docs google_docs googledocs
Google Drive google_drive googledrive
Google Sheets google_sheets googlesheets
Granola granola granola_mcp
HubSpot hubspot hubspot
Intercom intercom intercom, intercom_mcp
Jotform jotform jotform
Linear linear, linear_app linear, linear_mcp
Miro miro_custom_app miro
monday.com monday monday, monday_mcp
Neon neon_api_keys neon, neon_mcp
Notion notion, notion_api_key notion, notion_mcp
PayPal paypal paypal
PostHog posthog posthog, posthog_mcp
Sentry sentry sentry, sentry_mcp
Stripe stripe stripe, stripe_mcp
Supabase supabase, supabase_management_api supabase, supabase_mcp
Vercel vercel_token_auth vercel, vercel_mcp
Webflow webflow webflow
Wix wix_api_key wix, wix_mcp

A move needs permission to change the project’s connectors.

In the web app, open the connector in Customize → Connectors. The Connected tab names the connectors that can move. The connector page offers Move to Kortix. Sign in to the app when it opens.

From the CLI:

kortix connectors moves                  # each Pipedream and Composio connector and its Kortix app
kortix connectors move issues            # sign in once; the connector switches
kortix connectors move issues --connection <connection-id> --no-open
  • moves prints SLUG, PROVIDER, APP (the app on that provider), MOVES TO, STATE and ACCOUNTS. STATE is ready, signing in, signed in, or no Kortix app yet. --json prints the API response.
  • move opens the app’s sign-in in your browser and waits up to 10 minutes for it. --no-open prints the URL only. --device uses the device flow on a server that offers it.
  • --connection signs in on that account. Without it, move uses the connector’s shared project account.
  • Without connectors_v2, both commands exit 1 and print kortix projects features enable connectors_v2.

Through the API:

GET /v1/projects/{projectId}/connector-moves
{
  "moves": [
    {
      "slug": "issues",
      "name": "Issues",
      "provider": "composio",
      "app": "linear",
      "to": { "slug": "linear", "name": "Linear", "icon": "/connector-icons/linear.svg" },
      "connection_id": null,
      "signed_in": false,
      "accounts": 2
    }
  ]
}

to is null when the app has no Kortix equivalent on the deployment. connection_id is the account a started move signs in on. signed_in is true when that sign-in completed and only the switch is left.

POST /v1/projects/{projectId}/connectors/{slug}/move
Content-Type: application/json

{ "connection_id": "11111111-1111-4111-8111-111111111111" }

connection_id is optional. A connector that already moved answers { "status": "moved" }. Otherwise the answer is the next step:

{
  "status": "sign_in",
  "connection_id": "11111111-1111-4111-8111-111111111111",
  "to": { "slug": "linear", "name": "Linear", "url": "https://mcp.linear.app/mcp" }
}
  1. Sign in on connection_id with the OAuth routes: POST /v1/projects/{projectId}/connections/{connection_id}/oauth2/discover-resource, then /oauth2/register, then /oauth2/authorize. Open authorization_url. When discovery returns kortix_client: true, send register without registration_endpoint: Kortix signs in with its own OAuth client.
  2. Poll GET /v1/projects/{projectId}/connections/{connection_id}/oauth2/status until status is active. Kortix switches the connector when the sign-in stores its token.
  3. Wait for the switch: the connector leaves GET /v1/projects/{projectId}/connector-moves. If it is still listed with signed_in: true after 30 seconds, call move again. That call finishes a switch that failed and answers { "status": "moved" }.
Status Meaning
400 connection_id is an account of another connector.
403 feature_disabled without connectors_v2, no permission to change the project’s connectors, or no connection_id and no permission to manage the project’s accounts.
404 No connector with this slug, or connection_id is an account you cannot change.
409 The connector is not a Pipedream connector, its app has no Kortix equivalent on this deployment, or it has tool rules other than *. The message names the rules.
502 The kortix.yaml write failed. Call move again.

The SDK has the same two calls: p.connectors.listMoves() and p.connectors.move(slug, { connectionId }). See the SDK reference.

From the CLI

kortix connectors apps gmail                                   # search the managed catalog
kortix connectors add gmail --provider composio --app gmail --apply
kortix connectors connect gmail                                # authorize your account
  • Without --apply, add and rm edit your local kortix.yaml. Run kortix ship to apply the change, then kortix connectors sync to reconcile the connector.
  • With --apply, Kortix commits the change to the default branch and syncs the connector at once, like the web app.
  • kortix connectors rm <slug> [--apply] removes a connector. kortix connectors rename <slug> <name> sets its display name and applies at once.
  • kortix connectors catalog [<query>] and catalog show <id> browse the direct API catalog. They need the connectors_api_discover flag.
  • --auth-type none|bearer|basic|custom|oauth1 overrides the authentication Kortix detects from the source.

In kortix.yaml

connectors:
  - slug: gmail
    name: Gmail
    provider: composio
    app: gmail

  - slug: docs-search
    provider: mcp
    url: https://mcp.example.com/mcp

  - slug: billing-api
    name: Billing API
    provider: openapi
    spec: https://api.example.com/openapi.json
    auth:
      type: bearer
    headers:
      Accept: application/json
      X-Api-Version: "2026-01-01"
    policies:
      - match: "get_*"
        action: always_run
      - match: "*"
        action: require_approval

A change to connectors: applies when it reaches the default branch, through a change request merge or a direct push. A merge syncs at once. After a push, Kortix syncs on its next periodic pass. Run kortix connectors sync to apply it now. See Change requests.

Field Rule
auth.type bearer, basic, custom, api_key, oauth1, hmac, aws_sigv4, mtls or none. Omit auth on a direct provider and Kortix detects it. none opts out. oauth1 works on openapi, postman and http only.
auth.in, auth.name, auth.prefix Where a custom or api_key credential goes: in is header (default), query or cookie. name is required for both types.
auth on composio or channel Not allowed. The provider authorizes the account.
headers Static headers sent on every call: at most 32, names up to 128 characters, values up to 2,048 characters, no line breaks. Transport headers such as Host and Content-Length are refused. The auth header always wins over a static header of the same name. Not allowed on composio, pipedream or channel. Values are plaintext in git: never put a credential in a header.
sensitive: true Every action that no rule matches needs approval, reads included. See Policies and approvals.
enabled: false Kortix keeps the connector but refuses its calls with connector_disabled.
Reserved slugs kortix_slack, kortix_teams and kortix_email belong to channel connectors. computer belongs to the built-in computer connector. A new connector added from the web app, --apply or MCP also cannot take slack or email (400): connect Slack with kortix channels connect.

The full schema is in the kortix.yaml reference.

Connectors, connections and accounts

A connection is one account: one authorized identity for a connector, such as an OAuth grant or an API key. A connector can hold several accounts, for example a team inbox and your own inbox. Every account uses the connector’s policies.

Each account has an owner type that decides who may run a call as it:

Account owner_type Who can use it
Shared project Everyone in the project, and every agent whose connectors grant lists the connector, triggers included. You can narrow it to specific people, groups or agents.
Private member Only its owner, and only in the owner’s private sessions. An unattended run (a trigger, or code that uses a service account key, kortix_sa_…) never uses a private account.

connected_as is the identity the provider reports for an account, such as an email address or login. It shows on the Accounts tab, in kortix connectors accounts <slug>, and in kortix connectors connections ls. An account with a default label takes connected_as as its label. A label someone chose never changes.

Rename an account without a new authorization:

kortix connectors connections rename <connection-id> Support inbox

In the web app, open the account row’s menu and select Rename. The label cannot be me, project or UUID-shaped (400), and it must be unique among the same owner’s accounts, ignoring case (409).

Authorize an account

Add account on a connector page asks for a name and Who can use it:

  • Everyone in project creates a shared account.
  • Only you creates a private account. It is selected by default.
  • Specific people, groups, or agents creates a shared account for that audience only.

Then sign in to the app, or enter its API key.

A connector with exactly one shared account uses it as its default. Kortix makes an empty shared account when it adds a connector, except an MCP server added for Only you. The Accounts list does not show that empty account, so the connector reads has no account until someone adds one. While that account is still not signed in and is the only shared account, a new shared account reuses it instead of adding a second one. It keeps its name unless you type another. Two shared accounts with no default pinned leave the connector with no default. A sign-in on a shared account that is not the default does not reload an MCP connector’s tools.

When you have no account of your own on a connector, the Sign in, Set credential or Connect button at the top of the connector page opens Add account. It never signs a shared account in with your login. To sign in a shared account, select Connect on its row.

Method Web app CLI
Managed OAuth (composio) Add account, then sign in. kortix connectors connect <slug>
API key or token Add account, then enter the key. kortix connectors credential <slug> [value]. Without a value it prompts. - reads stdin.
Project secret — kortix connectors secret <slug> <identifier>. --clear removes the binding.
OAuth 2.1 (mcp) or OAuth2 (direct APIs) Add account, then sign in. kortix connectors connect <slug> on an MCP server with one-click sign-in. Otherwise kortix connectors authorize <slug>. See OAuth for MCP servers and direct APIs.
kortix connectors connect gmail                          # a private account (--owner me, the default)
kortix connectors connect gmail --owner project          # a shared account
kortix connectors connect gmail --label "Support inbox"  # name a new account
kortix connectors connect-finalize gmail                 # confirm; pass the same --owner
  • connect prints a link. Open it and approve the app. connect-finalize exits 1 while authorization is incomplete.
  • connect, --label and agent connect links work on composio connectors and on mcp connectors with one-click sign-in. One-click sign-in means the server publishes OAuth metadata and supports dynamic client registration, or it is a catalog app on a Kortix OAuth client. For such an mcp connector, the link is the app’s own consent page: Kortix runs discovery, client registration and PKCE on the server. A legacy pipedream connector gets a link for its shared account only.
  • --owner project needs permission to change the project’s connectors.
  • --label names a new account. The person who opens the link confirms the name and who can use it.
  • A project secret works as the credential when it is an active project secret, not a personal one, stored with --scope connector: kortix secrets set BILLING_KEY=- --scope connector. A binding answers 409 when the connector needs no credential, is a channel, or already has a stored credential, or when the secret is missing, inactive or not connector-scoped. A secret narrowed to some people works only in their private sessions (credential_not_shared). See Secrets.
  • A connect link for an account that is already authorized shows Already connected. To change the login, add a new account, make it the default, then disconnect the old one.

A session starts even when a granted connector has no account. The first call is denied with connector_not_connected and a connect_url. The web transcript shows it as a Connect button. This works on composio connectors and on mcp connectors with one-click sign-in. The button opens the Add account dialog with the name and audience the agent suggests. By default the account is private. Kortix tells the agent the new account’s name. For a shared account from a terminal, run kortix connectors connect <slug> --owner project.

OAuth for MCP servers and direct APIs

Kortix discovers the server’s OAuth metadata (RFC 9728, RFC 8414), registers itself as a client when the server supports dynamic client registration (RFC 7591), and runs PKCE (S256) with a resource parameter (RFC 8707). With registration support you create no OAuth app.

When the server does not support registration, the web app opens credential entry. Enter the client ID (and secret) of an OAuth app you create with the provider, and register this redirect URI with it:

https://api.kortix.com/v1/connectors/oauth2/callback

On a self-hosted instance the URI is <KORTIX_URL>/v1/connectors/oauth2/callback. KORTIX_URL must be stable. See Self-hosting.

From a terminal or an agent session:

kortix connectors authorize docs-search --json     # prints authorization_url
kortix connectors authorize docs-search --status   # exits non-zero on error
Flag Effect
--device Uses the device flow (RFC 8628): prints a code and a URL, then polls until approval, denial or expiry.
--scope "a b" Requests only these scopes.
--client-id, --client-secret Uses an existing OAuth app, for servers without dynamic registration.
--success-redirect, --error-redirect Where the browser lands after approval or denial. Without them the browser shows Connected. You can close this tab and return to Kortix., or the failure with its reason code.

Kortix refreshes the connector’s tool catalog when authorization completes. Direct API connectors also support the client credentials grant (client_secret_basic, client_secret_post, client_secret_jwt, private_key_jwt). Kortix refreshes access tokens before they expire and stores each rotated refresh token.

Share accounts and give agents access

Two checks decide whether a call can run: the agent’s grant to the connector, and the caller’s right to use the account.

Give an agent a connector

An agent reaches a connector only when agents.<name>.connectors in kortix.yaml lists its slug or is all. An omitted value means none.

agents:
  release-bot:
    connectors: [github, linear]
kortix agents scope release-bot --connectors github,linear

A change applies to a running session on its next call. The channel connector that started a session (Slack, Teams or email) is always callable from that session, so the agent can always answer where it was asked. See Agents & permissions.

Narrow a shared account

A shared account is open to everyone in the project until you narrow it. Select Share on the account and add groups, people or agents. Nothing changes until you select Save.

kortix access grant --group <group-id> --connection <connection-id>
kortix access grant --user <email> --connection <connection-id>
kortix access revoke <assignment-id>
  • A person in the audience uses the account only in a private session.
  • An agent in the audience uses it in all its sessions, triggers included. Anyone who can run that agent can use the account through it.
  • A session that binds a narrowed account cannot be shared, unless the account is shared with that session’s agent.
  • Only a person changes an audience. An agent session gets 403.
  • People outside the audience do not see the account. Connection managers see it marked Not shared with you.
  • Removing the last group or person opens the account to everyone again.
  • Only a shared account can be narrowed. A private account answers 404.

Share your private account

Share on your own Only you account turns it into a shared account for the people and groups you pick, or for everyone. Connection managers can then manage it too.

kortix connectors connections share <connection-id> --group <group-id>
kortix connectors connections share <connection-id> --everyone

Sharing needs permission to manage the project’s connections. A shared account with the same name answers 409: rename one first.

Pin an account for one session

A session can pin one account per connector for every call it makes:

kortix sessions new --connector gmail=<connection-id>
kortix sessions scope <session-id> --connector gmail=<connection-id>

--no-connectors removes the pins. A change applies on the next call, with no restart.

Which account a call uses

A call that names no account, or names me or project, uses an account only when the choice is certain:

  1. Exactly one account is reachable: the call uses it.
  2. Several are reachable and exactly one is pinned as the default: the call uses the default.
  3. Otherwise the call is denied with account_required and available_accounts.

me limits the choice to your private accounts. project limits it to shared accounts.

kortix connectors accounts gmail                         # accounts you may use, default first
kortix connectors accounts gmail --default "Support inbox"
kortix connectors call gmail <action> '{}' --account "Support inbox"
  • --account takes a label (any case), a connection id, me or project.
  • A named account that does not match is denied with connector_not_connected and the names you can use. Kortix never substitutes another account.
  • Every result carries account: { connection_id, label, owner_type }, so the transcript shows which identity ran the call.

Policies and approvals

A policy decides what happens when an action is called: Allow (always_run), Ask (require_approval) or Block (block). Kortix evaluates each call in this order, and the first match wins:

  1. Project-wide policies: in kortix.yaml. Patterns are fully qualified (gmail.send_email, *.delete*). A connector rule cannot override them.
  2. The connector’s own policies:. Patterns are relative to the connector (send_*).
  3. sensitive: true on the connector: every remaining action needs approval, reads included.
  4. policy.default_mode: allow_all (the default) runs every remaining action. risk runs reads and holds writes and destructive actions for approval.

Patterns are globs, case-insensitive, or /regex/ in slashes. A rule can also test the call’s arguments:

policy:
  default_mode: risk

policies:
  - match: gmail.send_email
    action: always_run
    conditions:
      - arg: to
        match: "*@example.com"
  - match: gmail.send_email
    action: require_approval
  • arg is a dot path into the arguments (message.channel). negate: true inverts a condition. A rule holds up to 10 conditions, and all must match.
  • A missing argument never matches the pattern, so a negated condition on it holds. An array matches only when every element matches.
  • A malformed condition fails closed: it never opens an action.
kortix connectors policy ls                                   # project-wide rules
kortix connectors policy set --default risk --apply           # or allow_all; without --apply it edits the local kortix.yaml
kortix connectors policy add "gmail.send_*" ask --condition to='*@example.com'   # project-wide rule; k!=v negates
kortix connectors policy rm "gmail.send_*"
kortix connectors policy gmail set send_email block           # connector rule: allow, ask or block
kortix connectors policy gmail ls                             # also: rm <match>, clear
kortix connectors sensitive gmail on                          # or off

policy add, policy rm, policy <slug> … and sensitive apply at once. The Tools tab on a connector page edits the same connector rules.

Approve a held call

A held call returns 202 with status: "pending_approval", approval_url, approval_summary and execution_id. Nothing ran.

  1. A person opens the approval in Review → Needs you, through approval_url, or on the approval card in a Slack or Teams thread. The page asks for a signed-in Kortix account that can approve actions in the project.
  2. The page shows the redacted arguments the provider will receive. They decide Approve or Deny, with an optional message back to the agent.
  3. Kortix sends the decision into the session, and the agent continues.
  • An approval covers one exact request: the same connector, action, arguments and account. A changed argument needs a new approval.
  • After an approval, the identical call runs once when it repeats within 15 minutes.
  • A held call expires when nobody decides it within 24 hours, or within the project’s approvals.ttl. approval_url expires at the same time. An expired call leaves Review → Needs you, its chat card shows Expired, and POST /v1/projects/<project_id>/approvals/<execution_id> returns 409 with code: "APPROVAL_EXPIRED". The session does not resume. The agent asks again with a new call.
  • There is no session-wide approval. An action that must run unattended needs an Allow rule.
  • A project manager or the person who started the session decides a held call. A session started with an API token (the CLI, a service account) is decided by project managers only.
  • Anyone else who can open the session sees the call and who decides it, with no Approve or Deny. They can copy approval_url and send it to that person: the approval page answers them with 403 too. In Review, the call is under Waiting for them, not Needs you.
  • GET /v1/projects/<project_id>/sessions/<session_id>/audit and GET /v1/projects/<project_id>/review/items return can_resolve and resolvers on each pending call. can_resolve is true only when the caller’s POST /v1/projects/<project_id>/approvals/<execution_id> would succeed. Any other caller gets 403. resolvers is managers or managers_and_launcher.
  • --reason (SDK: approvalContext) describes the effect when the arguments are only ids, for example who a draft goes to. The approver sees it marked as unverified. Kortix never sends it to the provider.

Call a connector from a session

An agent calls connectors with the kortix CLI in its sandbox. The same commands work from your terminal.

kortix connectors ls                            # connectors in scope and their status
kortix connectors discover "send an email"      # find an action by intent
kortix connectors show gmail.send_email         # one action's input schema
kortix connectors call gmail send_email @mail.json --account "Support inbox" --reason "Weekly report to the team"

call takes the arguments as inline JSON, @file.json, or - for stdin. --account, --reason, --attach and --out are optional.

Read the result

  • Read output ?? data. data is the raw upstream answer. output is the payload without the provider’s envelope, present only when it differs from data.
  • upstream_error reports a failure inside a 2xx answer (an MCP isError result, GraphQL errors). ok stays true.
  • An upstream 429 or 503 keeps its status. When the provider sent Retry-After, the body carries retry_after_seconds. Wait, then call again.
  • Every other upstream failure answers 500 with the cause in reason.
  • The upstream deadline is 60 seconds (upstream_timeout). Answers over 64 MiB are refused (upstream_response_too_large).
  • A call is not idempotent. After a timeout it may have run upstream. Check the effect before you repeat a write.

For a list or search result too large to read, write it to a file:

kortix connectors call linear list_issues '{"first":250}' --out /workspace/output/issues.json
jq '.data' /workspace/output/issues.json

--out writes the full JSON and prints only saved_to, bytes and the shape of data (keys, array lengths, pageInfo).

Attach files

Arguments never carry file bytes. Stage the file, and Kortix writes the bytes into the provider request server-side.

kortix connectors call outlook send_mail @mail.json --attach /workspace/artifacts/report.pdf
kortix connectors upload /workspace/artifacts/report.pdf --connector outlook   # prints ref
  • --attach appends a reference to the action’s attachments array. Name the array with --attach-path body.message.attachments when Kortix cannot find it.
  • upload returns ref, {"$kortix_attachment": "<id>"}. In an attachments array it becomes the provider’s attachment item. In a string field it becomes the file’s base64. On the email channel it becomes a signed download URL.
  • Files come from /workspace/output, /workspace/artifacts, /workspace/reports or /workspace/deliverables. Symbolic links and hard links are refused.
  • One call carries at most 20 files and 25 MiB. A staged file is single-use, private to its session, and deleted after 24 hours.
  • composio, pipedream and computer connectors do not accept staged files (connector_attachments_unsupported).

From an MCP client, the Kortix MCP server offers the same flow through list_connectors, search_connector_actions, describe_connector_action, call_connector and upload_connector_attachment.

Call connectors from code

Code calls a connector through @kortix/sdk and holds only a Kortix credential. Do not put a provider API key in an App or a backend when a connector for that provider exists: the key skips policies, approvals and audit.

Pick the credential

Where the code runs Credential Acts as Accounts it reaches
An App, in the browser createKortix({ backendUrl: '/_kortix/api/v1', getToken: kortixAppViewerToken() }). Set the App to kortix apps access <app> --viewer api. The person viewing the App Shared accounts that person may use, and their private accounts
An App, on its server await createAppViewerKortix(request, { backendUrl }) from @kortix/sdk/server. Build one client per request. The person viewing the App The same
Unattended code (a backend job, a cron, an App job) A service account key, kortix_sa_… The service account Shared accounts nobody narrowed. Never a private or narrowed account
A script or CI job A personal access key, kortix_pat_…. Bind it with kortix tokens new <name> --project <project-id>. You Shared accounts you may use, and your private accounts
A session sandbox KORTIX_TOKEN The session Limited by the agent’s connector grant
  • A viewer token reaches every project the viewer can read. Pass the App’s own project id on every call.
  • Create a service account with kortix tokens service-accounts new <name>. The key prints once. Give it a role with kortix access grant --service-account <id> --role member --project <project-id>: until then every call answers 403. kortix tokens service-accounts disable <id> revokes it. See API keys and tokens.
  • For the viewer token, see Know who is looking.

The connector handle

import { createKortix } from '@kortix/sdk';

const kortix = createKortix({ backendUrl: 'https://api.kortix.com/v1', getToken });
const crm = kortix.project(projectId).connector('crm');

const deals = await crm.run('list_deals', { stage: 'won' }, { account: 'Team CRM' });

kortix.connector(slug) is the same handle for a token bound to one project, such as a session token.

Method Returns
run(action, args, options) The action’s output. Throws a typed error for every other outcome.
call(action, args, options) The raw result (ok, data, output, account, binding, …). 200 and 202 resolve. Other statuses throw ApiError.
describe(), describe(action) The connector or one action with input and output schemas, or null.
accounts() The accounts this credential may use, default first.
paginate(action, args, { next, maxPages }) An async iterator over each page’s output.
uploadAttachment(content, { filename, contentType }) A ref for a later call of this connector.
Option Value
account A label, a connection id, me or project. Omit it for the account rule.
approvalContext Text for the approver. Never sent to the provider.
timeoutMs Client deadline. Default 90,000 ms, above the gateway’s 60 s upstream deadline.
signal An AbortSignal. The call may still run upstream.

Typed calls

kortix connectors types --out src/kortix-connectors.d.ts
kortix connectors types --connector linear,github --out src/kortix-connectors.d.ts

The file fills ConnectorActionRegistry in @kortix/sdk. Include it in tsconfig.json, and run and kortix.project(projectId).connectors.callAction(slug, action, args) type-check arguments and output. composio and pipedream connectors publish no output schema, so their output is unknown. Run the command again after you add a connector.

Errors in code

import { ConnectorApprovalPendingError, ConnectorCallError } from '@kortix/sdk';

try {
  return await crm.run('create_deal', { name: 'Renewal' });
} catch (error) {
  if (error instanceof ConnectorApprovalPendingError) return showLink(error.approvalUrl);
  if (error instanceof ConnectorCallError && error.connectUrl) return showConnect(error.connectUrl);
  if (error instanceof ConnectorCallError && error.retryAfterSeconds) return retryLater(error.retryAfterSeconds);
  throw error;
}
  • ConnectorApprovalPendingError means a policy held the call and nothing ran. It has approvalUrl, approvalSummary, approvalInstructions, executionId and retryable. Show the link. Do not repeat the call in a loop.
  • ConnectorCallError extends ApiError. code is upstream_<status> for an upstream failure, upstream_error for a failure inside a 2xx (status 200), or the gateway reason from Errors. It also has reason, hint, connectUrl, availableAccounts, requestedAccount, upstreamStatus, retryAfterSeconds and binding.
  • The SDK retries nothing, because a connector call is not idempotent.

Pages

import { ConnectorPageLimitError } from '@kortix/sdk';

try {
  for await (const page of crm.paginate('list_deals', { limit: 100 }, {
    next: (page, args) => (page.next_cursor ? { ...args, cursor: page.next_cursor } : undefined),
    maxPages: 20,
  })) await save(page.deals);
} catch (error) {
  if (error instanceof ConnectorPageLimitError) await resumeLater(error.nextArgs);
  else throw error;
}

next returns the next page’s arguments, or undefined after the last page. maxPages defaults to 100. When pages remain after it, paginate throws ConnectorPageLimitError (code: 'max_pages_exceeded') with nextArgs, so a capped listing never reads as complete. Each page is one call, with its own policy check and audit event.

In React, useConnectorQuery(projectId, slug, action, args) from @kortix/sdk/react reads one action as a cached query. Use it for read actions only. See React hooks.

Errors

A denied call answers { ok: false, status: "denied", reason } plus a hint or message that names the fix. A failed call answers { ok: false, status: "error", reason, upstream_status }.

An error reason starts with the code in the table. Text after the first : is detail (upstream_timeout: …). For an upstream failure, read upstream_status: reason can be the provider’s own text. In the SDK, ConnectorCallError.code is always the bare code or upstream_<status>.

reason HTTP Meaning Fix
connector_not_assigned 403 The agent’s connectors: grant does not list this connector. The body names agent and granted. Add the slug to agents.<name>.connectors.
connector_not_connected 403 No usable account, or --account named one that does not exist. Open connect_url, or retry with a name from available_accounts.
account_required 403 Several accounts are reachable, none is named, none is pinned. Pass --account, or pin a default.
credential_not_shared 403 The connector’s project secret is not shared with the person this call acts for. Ask the secret’s owner to share it.
credential_undecryptable 403 Kortix cannot decrypt the connector’s stored credential, or the project secret it uses: the server’s encryption key changed, or the stored value is damaged. message names the connector or the secret. Reconnect the account, or enter the credential or the secret again. For an OAuth 2.0 account, save its OAuth application again first: the same key encrypts it, and until then its routes answer 409 SECRET_UNDECRYPTABLE.
connector_disabled 403 The connector is declared with enabled: false. Enable it.
policy_block 403 A Block rule matched. Final. Do not retry. Change the policy.
connector_not_found 404 No connector with this slug. Run kortix connectors ls.
action_not_found 404 The connector has no such action. Run kortix connectors show <slug>.
computer_* 403 or 409 The computer refused, is offline, or is not paired. See Computers.
insufficient_scope 403 An OAuth token without the kortix scope. Use a token with the kortix scope.
viewer_api_disabled 403 An App’s browser call while the App is not --viewer api. kortix apps access <app> --viewer api.
upstream_<status>, upstream_timeout, upstream_response_too_large 500 (429 and 503 keep their status) The provider failed, did not answer in 60 s, or answered over 64 MiB. Wait for retry_after_seconds, or ask for a smaller page. Check the effect before you repeat a write.
connector_egress_blocked 500 The target resolves to a private or internal address. See Network safety.
connector_attachments_unsupported 500 A staged file on a composio, pipedream or computer connector. Send the file through the provider’s own input.

Network safety

Kortix checks every connector request before it leaves: calls to http, openapi, graphql, mcp, postman and channel connectors, and the catalog reads of a sync.

  • The host must resolve to public addresses. A private or internal address is refused with connector_egress_blocked.
  • Kortix follows redirects itself and checks each hop again. A redirect to another origin drops the Authorization header.
  • Both http and https are allowed.

A self-hosted instance can allow internal hosts with KORTIX_CONNECTOR_EGRESS_ALLOW_HOSTS. Managed apps that need your own OAuth app, and the other integration settings, are on Self-hosting.

Was this page helpful?