--- title: 'stackbone.connection' description: 'Call a connector (Gmail, Linear, GitHub, …) from your agent or workflow with Stackbone Connect: the credential never enters your container.' position: 17 --- # stackbone.connection > **The credential never enters your agent container.** The operator > registers a connector and its credentials once in Studio. Your code > reaches that connector through the Stackbone Connect **broker**, which > mints a short-lived scoped token at call time and runs the action on > your behalf. Your code only ever sees the connector id and the > operation output, never a token or key. A **connector** is a typed integration with a third party (Gmail, Linear, GitHub, an internal OpenAPI service). Stackbone Connect is the way your agents and workflows act on those connectors. There are three entrypoints, each on its own import. The one you use depends on where you call from: | You are in… | Use | Import from | | ------------------------------------ | ---------------------------------------------------- | ------------------------- | | an agent tool | `stackbone.connection(id)` | `@stackbone/sdk` | | a durable workflow `step` | `callConnector(...)` (or `stackbone.connection(id)`) | `@stackbone/sdk/workflow` | | a connection runtime's `auth` config | `connect()` / `withConnect()` / `connectHeaders()` | `@stackbone/sdk/connect` | The first two are imperative "call this operation now" handles. The third wires a connector's auth into a connection definition your own tool code builds (an MCP client, a generated OpenAPI client), so its calls reach the provider without you passing each call through the broker yourself. All three hit the same broker over the same HMAC-signed transport, so pick the one that matches your call site. For the broker model and how an operator installs credentials, see [Concepts → Stackbone Connect](/docs/home/features/integrations). ## Call a connector from your code: `stackbone.connection(id)` The ambient [`stackbone`](/docs/sdk/reference/overview) client carries a `connection(id)` accessor. Select a connector by its **verbatim id** (the same id the operator gave it in Studio) and you get a handle whose operations you can call: ```ts import { tool } from '@langchain/core/tools'; import { stackbone, z } from '@stackbone/sdk'; const notifyOps = tool( async ({ raw }: { raw: string }) => { // Gmail's operation ids are dotted, so they are not JS identifiers: call one // through `.call(operation, args)`, which is always available. await stackbone.connection('gmail').call('gmail.users.messages.send', { userId: 'me', requestBody: { raw }, }); // An operation whose id IS a plain identifier can be called directly, and // is fully typed once `stackbone dev` has generated your connector types: await stackbone.connection('stub-mail').sendMail({ to: 'ops@acme.com', subject: 'ping' }); return JSON.stringify({ sent: true }); }, { name: 'notify_ops', description: 'Email the ops inbox.', schema: z.object({ raw: z.string() }), }, ); ``` Two things to know about this handle: - Operations become typed once `stackbone dev` has generated your workspace's connector types (written to `.stackbone/connect.d.ts`). With types in place a typo becomes a compile error and your editor autocompletes the real operations. Until then (or for an operation id that is not a valid JS identifier, like a dotted `chat.postMessage`), use the always-present `.call(operation, args)` escape hatch. - It returns the operation output as plain JSON (`Promise`), so narrow it yourself. There is **no `Result` envelope** on this path: on failure it throws (see [Errors](#errors)). This handle is a plain HMAC-signed call to the broker. It pulls in no extra dependency, so a project can import `@stackbone/sdk` and call connectors without installing the agent-authoring or Workflow SDK peers. ## Call a connector from a workflow step: `callConnector()` Inside a durable workflow, a `'use step'` often needs to do one provider action (send a Telegram message, read a file from GitHub) with no agent in the loop. `callConnector()` is that direct path. Import it from `@stackbone/sdk/workflow`: ```ts import { callConnector } from '@stackbone/sdk/workflow'; async function notifyTeam(orderId: string) { 'use step'; // runs once, persisted, retried on failure. Keep it idempotent await callConnector('telegram', 'send-message', { chat_id: '123456', text: `New order ${orderId}`, }); } ``` `callConnector(connector, operation, args?, opts?)` POSTs the action to the broker and returns the operation output as plain JSON (`Promise`). It throws a `ConnectorCallError` on failure: same contract as `stackbone.connection(id).call(...)`, which is sugar over it. The optional `opts.principal` chooses **who** the broker makes the call on behalf of. It defaults to `{ type: 'app' }`: the agent's own service-account credential, which almost every call should use: ```ts // Explicit, equivalent to the default: await callConnector('telegram', 'send-message', args, { principal: { type: 'app' } }); ``` The principal can also be `user`, `client-credentials`, or `jwt-bearer` for the broker's machine-to-machine modes; the `user` principal scopes the broker's token issuer to a specific end user. App-scoped is the path you want for an agent acting as itself. Workflows and durable steps are described in [Concepts → Workflows](/docs/sdk/workflows/overview) and the [Workflow SDK docs](https://workflow-sdk.dev/docs). ## Wire broker auth into a connection runtime: `connect()` The two handles above call a connector imperatively. The other model applies when your tool code builds on its own connection-authoring library (an MCP client, a generated OpenAPI client) that probes an `auth` strategy before every call. Wire that strategy to the Stackbone Connect broker instead of holding a static credential. `connect(connectorId)` returns that auth strategy. Import it from `@stackbone/sdk/connect`: ```ts // deep-agents/support/connections/example.ts import { defineOpenAPIConnection } from ''; import { connect } from '@stackbone/sdk/connect'; export default defineOpenAPIConnection({ spec: 'https://api.example.com/openapi.json', auth: connect('example'), // broker mints a short-lived bearer at call time }); ``` The connection runtime probes `auth` before every call; `connect()` fetches a fresh short-lived bearer from the broker each time, so a rotated credential takes effect on the next call and your code holds nothing static. The returned strategy is a plain `{ getToken, evict }` shape and drops into any connection factory that accepts one, for example `defineMcpClientConnection({ auth: connect('…') })`. Two helpers cover the common variations: - `withConnect(definition, connectorId)` bolts broker auth onto an existing connection definition without re-typing the `auth` field: ```ts import { withConnect } from '@stackbone/sdk/connect'; export default withConnect( defineOpenAPIConnection({ spec: 'https://api.example.com/openapi.json' }), 'example', ); ``` - `connectHeaders(connectorId)` is for a credential that does **not** go in as a Bearer token. A connection runtime's `auth` carries only a Bearer; anything else rides the connection's separate `headers` closure, and `connectHeaders` produces that closure, pre-wired to the broker: ```ts import { connectHeaders } from '@stackbone/sdk/connect'; export default defineOpenAPIConnection({ spec: 'https://api.example.com/openapi.json', headers: connectHeaders('example'), }); ``` **Where the token goes is the broker's answer, not yours.** The mint carries the placement the operator configured in Studio, and every slot gets the same token on the same request: a `bearer` placement becomes `Authorization: Bearer …`, a named `header` gets the raw token with no scheme, and a credential registered in two slots at once gets both. So `connectHeaders('linear')` sends a bare `Authorization`, without you naming the header. That is what Linear wants: it answers a `Bearer` prefix with a 400. The optional second argument is a **fallback**, not an override: it is used only when the box is old enough not to report its placement set, and it defaults to `X-Api-Key`. A credential placed in a query parameter cannot be a header at all, so it throws `ConnectionAuthorizationFailedError` with `reason: 'placement_unsupported'` rather than sending the request unauthenticated. Call that connector through `stackbone.connection(id).call(…)` instead, where the box applies the placement server-side. `connect()` and `withConnect()` are **Bearer-only** and cannot honour a placement: `TokenResult` has no field for one, so the runtime prepends `Bearer` unconditionally. Wire a non-Bearer connector through `connectHeaders()` or `stackbone.connection(id)`. > **Scope.** `connect()` resolves the agent's **own** (app-scoped) > credential: the operator completes the connector's OAuth setup once in > Studio and the broker hands the token over at call time. Interactive, > per-user OAuth is a future phase; today this path is app-scoped and > non-interactive. ## When the connector isn't ready If the operator hasn't finished installing a connector's credentials in Studio, `connect()` (and the helpers built on it) signals it with a typed error so you can branch instead of failing blind. Both are re-exported from `@stackbone/sdk/connect`: - `ConnectionAuthorizationRequiredError`: the credential needs to be authorized out of band (the operator must finish the OAuth dance in Studio). - `ConnectionAuthorizationFailedError`: a terminal credential failure (for example, the connector was never installed). Match these **by `err.name`, never `instanceof`**: under bundling your code and the SDK can end up with different class identities, so the name is the only stable signal: ```ts try { await stackbone.connection('example').call('invoices.get', { id: 'inv_123' }); } catch (err) { if (err instanceof Error && err.name === 'ConnectionAuthorizationRequiredError') { // Tell the operator to finish connecting the integration in Studio. } } ``` ## Errors The direct-call path (`stackbone.connection(id)` and `callConnector()`) does **not** return a `Result` envelope. On failure it **throws** a `ConnectorCallError`: an `Error` carrying a machine-readable `.code` from the broker. Match on the code string, **never `instanceof`** (the same dual-instance hazard applies): ```ts try { await stackbone.connection('gmail').call('gmail.users.messages.send', { userId: 'me' }); } catch (err) { const code = err instanceof Error ? (err as { code?: string }).code : undefined; if (code === 'connector_installation_required') { // Operator hasn't installed this connector's credentials yet. } } ``` Common codes: `invalid_args`, `credential_error`, `no_token`, `timeout`, `connector_installation_required`, `execute_failed`, `invalid_output`. ## What the runtime provides You never sign or address the broker yourself. The runtime does it: - It injects the **broker base URL** and the **install id** on the agent / workflow process. It HMAC-signs every connector call and scopes it to your install. - The broker resolves the credential server-side, runs the action, and returns only the output: the credential never enters your container. - A call made while the runtime is running your code carries the **id of the run** that made it. You pass nothing. It is what lets an [eval suite](/docs/cli/reference/eval) point a connector at a stand-in for its own runs while live traffic in the same organization keeps reaching the real provider. - Running outside a Stackbone runtime (no broker URL or install id on the process) throws an error telling you to run the agent through `stackbone dev`. The routes and the signature scheme are an implementation detail of the broker. ## Receiving connector events Calling an operation is the **outbound** half. The **inbound** half, reacting when a connector fires (a message arrives, a new email lands), does not go through these handles. The operator wires a connector trigger to your workspace in Studio; when it fires, it **starts a durable workflow run** (or an agent session), which then does its work and can call connectors back out. There is no synchronous handler to import. To develop a trigger-driven workflow locally without a real account, start the workflow directly by name with [`stackbone workflows start`](/docs/cli/reference/workflows#stackbone-workflows-start). ## Migrating from the bare `connection(id)` export The bare top-level `connection(id)` export is gone from `@stackbone/sdk/workflow`. Import the ambient client and call `stackbone.connection(id)` instead. On `@stackbone/sdk/connect` it survives as a **deprecated** alias that behaves the same. Prefer the namespaced form so every SDK capability reads as `stackbone.`. ## Where to go next - **[Calling a sibling agent](/docs/sdk/agents/calling-a-sibling-agent)**: `callDeepAgent()`, the closest surviving pattern for reaching another part of your workspace. - **[`@stackbone/sdk` overview](/docs/sdk/reference/overview)**: the ambient handle and the subpath import map. - **[Concepts → Stackbone Connect](/docs/home/features/integrations)**: the broker model and how an operator installs credentials. - **[`stackbone workflows start`](/docs/cli/reference/workflows#stackbone-workflows-start)**: start a durable workflow run by name in your workspace under `stackbone dev`. - **[Workflow SDK](https://workflow-sdk.dev/docs)**: the upstream durable execution engine Stackbone workflows build on.