<skill name="kortix-connectors">
<overview>
A **connector** defines tools against an external system. A connector is
**not** an account: one connector (e.g. Gmail) can hold several **accounts** —
each one SHARED with the whole project or PRIVATE to one member. A
**connector call** invokes one tool AS one account.
Use the kortix connectors CLI for normal agent work:
kortix connectors ls lists connectors and actions (an ACCOUNTS column
shows how many each holds).
kortix connectors discover "<intent>" searches visible actions.
kortix connectors show <connector>.<action> shows one input schema and risk.
kortix connectors accounts <slug> lists the accounts a connector holds,
default first — see Choosing the account below before your first call
on a connector you have not used yet.
kortix connectors call <connector> <action> '<json>' [--account <label>]
invokes one action. Every successful result echoes account: say which one
ran when it matters.
kortix connectors call … --reason "<what this does>" tells the human what
the call does if a policy holds it for approval. The approver sees it next
to the real arguments. Always pass it on a write whose args are only ids
— send_draft (say who it goes to, the subject, and the body), a delete (say
what gets deleted), a merge. Without it the approver sees only draft_id.
A gated call returns approval_url: share it, then stop the turn. The human's
decision, and any message they add to it, arrives as your next prompt — a
deny with a message is an instruction, not a dead end. In a Slack or Teams
session Kortix posts an approval card in the thread itself
(approval_instructions says so): do not repost the link, just stop.
kortix connectors call … --attach <file> attaches a file (see Attach
files below). Never put base64 in args.
kortix connectors call … --out <file> writes the full JSON result to
<file> and prints only saved_to, bytes, and shape (keys, array
lengths, pageInfo). Use it for list and search calls; tool output above
~50 KB is truncated. Then query the file with jq or bun, never cat.
kortix connectors add, rm, and connect manage connectors and connections.
Durable TypeScript workflows use @kortix/sdk and createKortix. Every
call runs through the connector gateway. The
gateway resolves credentials, enforces access and policy, invokes the upstream
system, and records an audit event. The sandbox carries KORTIX_TOKEN; it
does not carry raw third-party credentials.
</overview>
<when-to-load>
Load this skill when the user wants to:
- Act in an external app or API.
- Inspect available connectors, actions, or connected computers.
- Add or configure a connector or connection.
- Request connector credentials without exposing the value.
- Build a repeatable workflow that calls external systems.
Do not load it for work that stays inside the local repository or sandbox.
</when-to-load>
<choosing-the-account>
**A connector is not an account.** One connector (e.g. Gmail) can hold many
accounts — shared with the whole project, or private to one member. Never
assume "one connector, one account", and never infer which accounts exist
from a `get_profile`/`whoami`-style call: that only ever answers for the ONE
account it happened to run as. If a human asks which or how many accounts are
connected, list them — do not guess from a profile call.
Procedure, every time you call a connector you have not just listed accounts for:
- List the accounts:
kortix connectors accounts <slug> (default first;
(pinned default) marks the one an unnamed call uses).
- One account → just call. Nothing to choose.
- Several accounts, and the human named one → pass
--account <label|id|me|project> (me = your own private default,
project = the shared default).
- Several accounts, and it is unclear which one → ASK the human. Do not
guess, and do not silently use the default — several real accounts exist
and picking wrong sends the action to the wrong mailbox/workspace. If
nothing is named and nothing is pinned, the call is refused anyway with
reason
account_required rather than guessing; a human can pin one going
forward with kortix connectors accounts <slug> --default <label>.
Always report which account ran when it could matter — read it off the
result's account field, never assume.
Worked example. A project has one Gmail connector (gmail-ffiod0) with
two accounts:
$ kortix connectors accounts gmail-ffiod0
LABEL OWNER DEFAULT CONNECTION ID
personal@example.com private no 11111111-…
work@example.com private no 22222222-…
kortix connectors call gmail-ffiod0 <action> --account "personal@example.com"
kortix connectors call gmail-ffiod0 <action> --account "work@example.com"
kortix connectors call gmail-ffiod0 <action> --account me
kortix connectors call gmail-ffiod0 <action> --account project
2 accounts
No default pinned — unnamed calls will be refused with account_required;
pass --account or pin one: kortix connectors accounts gmail-ffiod0 --default <label>
Neither account is pinned, so — asked "check my gmail" with no account named —
the right move is to ASK which mailbox, not to call get_profile on whichever
account resolves first and report "one account connected". If the human says
"the work one", call with --account "work@example.com" and report: "Checked
work@example.com — …".
Adding another account. When the human wants a new one ("connect my other
Gmail"), mint a link with a --label that tells it apart:
kortix connectors connect <slug> --label "Personal Gmail". The link opens a
dialog where the human names the account and chooses who can use it; you are
then told its name. Call it with --account "<name>" from then on.
</choosing-the-account>
<cli-first-loop>
Use the CLI first. It is pre-authenticated in a session sandbox.
- List visible connectors:
kortix connectors ls
- Search by intent:
kortix connectors discover "send an email"
- Inspect one action before an unfamiliar call:
kortix connectors show email_email_inbox_bjgk.reply_message
- Call the action:
kortix connectors call email_email_inbox_bjgk reply_message \
'{"inbox_id":"email-inbox@agentmail.to","message_id":"<message-id>","text":"Reply text"}'
Attach files. Write the file under /workspace/artifacts (or
output, reports, deliverables), put the JSON args in a file, and pass
--attach. The CLI stages the raw bytes and the gateway builds the
provider's attachment item (Microsoft Graph, SendGrid, Postmark, Mailjet,
Resend, Brevo, and the Email channel):
cat > /tmp/mail.json <<'JSON'
{"user":"sender@example.com","body":{"message":{"subject":"Report",
"body":{"contentType":"Text","content":"Attached."},
"toRecipients":[{"emailAddress":{"address":"to@example.com"}}]},
"saveToSentItems":true}}
JSON
kortix connectors call microsoft-graph sendmail @/tmp/mail.json \
--attach /workspace/artifacts/report.pdf
Leave the attachments array out of the JSON; --attach appends to it. For a
field the CLI cannot find, run kortix connectors upload <file> --connector <slug> and put the printed ref ({"$kortix_attachment":"<id>"}) where the
file belongs: in an attachments array, or as the value of a base64 field
such as contentBytes. Pass the body as a JSON object, never as a string.
For GraphQL actions, put selected fields in args.__select:
kortix connectors call internal_graph query.user \
'{"id":"1","__select":"id name email"}'
</cli-first-loop>
<sdk-workflows>
Use `@kortix/sdk` for dependent calls, pagination, branching, retries, or
reusable scripts. Read `references/sdk.md` for the full pattern.
import { createKortix } from '@kortix/sdk';
const kortix = createKortix({
backendUrl: process.env.KORTIX_API_URL!,
getToken: async () => process.env.KORTIX_TOKEN ?? null,
});
const connectors = process.env.KORTIX_PROJECT_ID
? kortix.project(process.env.KORTIX_PROJECT_ID).connectors
: kortix.connectors;
const matches = await connectors.search('send an email', { limit: 5 });
const action = await connectors.describe(matches[0]!.tool);
if (!action) throw new Error('Email action not found');
const result = await connectors.call('email_email_inbox_bjgk.reply_message', {
inbox_id: 'email-inbox@agentmail.to',
message_id: '<message-id>',
text: 'Reply text',
});
if (!result.ok) {
throw new Error(`Connector call failed: ${result.reason ?? result.status ?? 'unknown'}`);
}
Run repository scripts with bun run path/to/script.ts. Keep provider
credentials out of code and repository files.
</sdk-workflows>
<from-apps-and-backends>
App, Convex backend, and external-program code calls connectors through
`@kortix/sdk` too. Never give that code a provider API key when a connector
for the provider exists: a raw key skips policy, approvals, and audit. The
Kortix credential decides which accounts a call reaches:
| Where the code runs | Credential | Reaches |
|---|
| App, browser | backendUrl: '/_kortix/api/v1' + kortixAppViewerToken() (App set to --viewer api) | the viewer's shared and private accounts |
| App, server | createAppViewerKortix(request, { backendUrl }) | the same, per request |
| Convex action, App job with no viewer | a service account bearer (kortix_sa_…) in an env var | shared accounts nobody narrowed; never a private one |
| External program, CI | a personal access token, kortix tokens new <name> --project <id> | the token owner's shared and private accounts |
A service account is a human step. An agent session cannot create one. Ask
the person to run kortix tokens service-accounts new <name> and
kortix access grant --service-account <id> --role member --project <id>,
and to store the bearer (npx convex env set KORTIX_API_KEY … for Convex; a
project secret mapped under apps.<slug>.secrets for an App, with a name that
does not start with KORTIX_). Build the rest while you wait. Recipes and
errors: references/sdk.md → "Calling from an App, a backend, or a script".
</from-apps-and-backends>
<adding-connectors>
Connector definitions live in `kortix.yaml`. Connections remain server-side.
connectors:
- slug: stripe
name: Stripe API
provider: openapi
spec: https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json
auth:
type: bearer
Use composio for managed SaaS apps. Other supported direct providers are
mcp, openapi, postman, graphql, and http. Add and connect a Composio
toolkit with:
kortix connectors add github --provider composio --app github --apply
kortix connectors connect github
Pipedream exists only for rollback compatibility with already-declared
connectors. Never add a new Pipedream connector automatically. If Composio
cannot satisfy the request, stop, explain the gap, and ask the human before any
explicit --allow-legacy-pipedream retry.
Surface the returned connection URL. For an API key you already have (the user
gave it in chat), store it now: kortix secrets set NAME=- --scope connector.
For one you lack, use kortix secrets request NAME --scope connector.
Slack uses the channel flow. Do not add a Slack connector. Run:
kortix channels connect
</adding-connectors>
<rules>
- A connector is not an account. One connector (e.g. Gmail) can hold many
accounts (shared or private) — see **Choosing the account** above.
- If the human asks which or how many accounts are connected, ALWAYS list
them with `kortix connectors accounts <slug>`. Never infer accounts from a
profile/whoami call.
- Use `kortix connectors` for one-off agent actions.
- Use `@kortix/sdk` for durable or testable workflows, and for App, Convex
backend, and external-program code (see **From apps and backends**).
- Use Composio for every new managed SaaS connector. Never select Pipedream
unless the human explicitly approves the legacy rollback path.
- Do not use raw provider tokens from the sandbox.
- Treat `denied`, `not_shared`, `needs_auth`, `account_required`, and
`ok: false` as real outcomes — `account_required` means several accounts
are reachable and none was named or pinned; pass `--account` or ask the
human, do not retry blind.
- Report which account ran when it could matter — read the result's
`account` field, never assume.
- Confirm irreversible work before a destructive connector call.
</rules>
</skill>