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
- Open Customize → Connectors.
- On the All tab, find the app and select Install.
- 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.
- 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
401or403) 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’sWWW-Authenticatechallenge. Without one, the401or403is 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
- Open Customize → Connectors and select New → MCP server.
- 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. - Choose Who can use it. Only you is selected by default.
- 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.
- Find the app and select Install.
- Connect app opens. Choose Who can use it and select Connect.
- 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_appstool of the Kortix MCP server lists the MCP apps, andadd_connectorwithappadds 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
sensitiveflag, and a*rule in itspolicies. A rule that names a tool (for examplesend_emailon the connector, orgmail.send_emailin 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.yamlentry toprovider: mcpwith the app’surl, 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.yamlcommit, 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,googlesuperorsupabase_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
movesprintsSLUG,PROVIDER,APP(the app on that provider),MOVES TO,STATEandACCOUNTS.STATEisready,signing in,signed in, orno Kortix app yet.--jsonprints the API response.moveopens the app’s sign-in in your browser and waits up to 10 minutes for it.--no-openprints the URL only.--deviceuses the device flow on a server that offers it.--connectionsigns in on that account. Without it,moveuses the connector’s shared project account.- Without
connectors_v2, both commands exit1and printkortix 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" }
}
- Sign in on
connection_idwith the OAuth routes:POST /v1/projects/{projectId}/connections/{connection_id}/oauth2/discover-resource, then/oauth2/register, then/oauth2/authorize. Openauthorization_url. When discovery returnskortix_client: true, sendregisterwithoutregistration_endpoint: Kortix signs in with its own OAuth client. - Poll
GET /v1/projects/{projectId}/connections/{connection_id}/oauth2/statusuntilstatusisactive. Kortix switches the connector when the sign-in stores its token. - Wait for the switch: the connector leaves
GET /v1/projects/{projectId}/connector-moves. If it is still listed withsigned_in: trueafter 30 seconds, callmoveagain. 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,addandrmedit your localkortix.yaml. Runkortix shipto apply the change, thenkortix connectors syncto 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>]andcatalog show <id>browse the direct API catalog. They need theconnectors_api_discoverflag.--auth-type none|bearer|basic|custom|oauth1overrides 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
connectprints a link. Open it and approve the app.connect-finalizeexits1while authorization is incomplete.connect,--labeland agent connect links work oncomposioconnectors and onmcpconnectors 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 anmcpconnector, the link is the app’s own consent page: Kortix runs discovery, client registration and PKCE on the server. A legacypipedreamconnector gets a link for its shared account only.--owner projectneeds permission to change the project’s connectors.--labelnames 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 answers409when 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.
Connect links inside a session
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:
- Exactly one account is reachable: the call uses it.
- Several are reachable and exactly one is pinned as the default: the call uses the default.
- Otherwise the call is denied with
account_requiredandavailable_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"
--accounttakes a label (any case), a connection id,meorproject.- A named account that does not match is denied with
connector_not_connectedand 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:
- Project-wide
policies:inkortix.yaml. Patterns are fully qualified (gmail.send_email,*.delete*). A connector rule cannot override them. - The connector’s own
policies:. Patterns are relative to the connector (send_*). sensitive: trueon the connector: every remaining action needs approval, reads included.policy.default_mode:allow_all(the default) runs every remaining action.riskruns 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
argis a dot path into the arguments (message.channel).negate: trueinverts 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.
- 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. - The page shows the redacted arguments the provider will receive. They decide Approve or Deny, with an optional message back to the agent.
- 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_urlexpires at the same time. An expired call leaves Review → Needs you, its chat card shows Expired, andPOST /v1/projects/<project_id>/approvals/<execution_id>returns409withcode: "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_urland send it to that person: the approval page answers them with403too. In Review, the call is under Waiting for them, not Needs you. GET /v1/projects/<project_id>/sessions/<session_id>/auditandGET /v1/projects/<project_id>/review/itemsreturncan_resolveandresolverson each pending call.can_resolveistrueonly when the caller’sPOST /v1/projects/<project_id>/approvals/<execution_id>would succeed. Any other caller gets403.resolversismanagersormanagers_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.datais the raw upstream answer.outputis the payload without the provider’s envelope, present only when it differs fromdata. upstream_errorreports a failure inside a2xxanswer (an MCPisErrorresult, GraphQLerrors).okstaystrue.- An upstream
429or503keeps its status. When the provider sentRetry-After, the body carriesretry_after_seconds. Wait, then call again. - Every other upstream failure answers
500with the cause inreason. - 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
--attachappends a reference to the action’s attachments array. Name the array with--attach-path body.message.attachmentswhen Kortix cannot find it.uploadreturnsref,{"$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/reportsor/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,pipedreamandcomputerconnectors 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 withkortix access grant --service-account <id> --role member --project <project-id>: until then every call answers403.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;
}
ConnectorApprovalPendingErrormeans a policy held the call and nothing ran. It hasapprovalUrl,approvalSummary,approvalInstructions,executionIdandretryable. Show the link. Do not repeat the call in a loop.ConnectorCallErrorextendsApiError.codeisupstream_<status>for an upstream failure,upstream_errorfor a failure inside a2xx(status200), or the gateway reason from Errors. It also hasreason,hint,connectUrl,availableAccounts,requestedAccount,upstreamStatus,retryAfterSecondsandbinding.- 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
Authorizationheader. - Both
httpandhttpsare 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.