Connections

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 (Slack, GitHub, Gmail, 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, and which 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 transparently instead of you passing a call through the broker yourself. They all hit the same broker over the same HMAC-signed transport, pick the one that matches your call site.

For the broker model and how an operator installs credentials, see Concepts → Stackbone Connect.

Call a connector from your code — stackbone.connection(id)

The ambient stackbone 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:

import { tool } from '@langchain/core/tools';
import { stackbone, z } from '@stackbone/sdk';

const postToSlack = tool(
  async ({ channel, text }: { channel: string; text: string }) => {
    // Typed once `stackbone dev` has generated your connector types:
    const result = await stackbone.connection('slack').chat_postMessage({ channel, text });

    // Dynamic escape hatch — always available, and required for an
    // operation id that is not a valid JS identifier (a dotted id):
    await stackbone.connection('slack').call('chat.postMessage', { channel, text });

    return JSON.stringify({ posted: true });
  },
  {
    name: 'post_to_slack',
    description: 'Post a message to the team Slack channel.',
    schema: z.object({ channel: z.string(), text: z.string() }),
  },
);

stackbone.connection(id) addresses a connector by the same verbatim id the operator gave it in Studio. Two things to know:

  • 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<unknown>). Narrow it yourself. There is no Result envelope on this path: on failure it throws (see 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 ever 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 Slack message, create a GitHub issue — with no agent in the loop. callConnector() is that direct path. Import it from @stackbone/sdk/workflow:

import { callConnector } from '@stackbone/sdk/workflow';

async function notifyTeam(orderId: string) {
  'use step'; // runs once, persisted, retried on failure — keep it idempotent
  await callConnector('slack', 'chat.postMessage', {
    channel: 'C123',
    text: `New order ${orderId}`,
  });
}

callConnector(connector, operation, args?, opts?) POSTs the action to the broker and returns the operation output as plain JSON (Promise<unknown>). It throws a ConnectorCallError on failure — same contract as stackbone.connection(id).call(...), which is just sugar over it.

The optional opts.principal chooses who the call is made on behalf of. It defaults to { type: 'app' } — the agent's own service-account credential — which is what almost every call should use:

// Explicit, equivalent to the default:
await callConnector('slack', 'chat.postMessage', 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 and the Workflow SDK 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 is built on a connection-authoring library of its own (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:

deep-agents/support/connections/example.ts
import { defineOpenAPIConnection } from '<your connection handler>';
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 is always current and nothing static is ever held in your code. The returned strategy is a plain { getToken, evict } shape, it drops into any connection factory that accepts one, for example defineMcpClientConnection({ auth: connect('…') }).

Two helpers cover the common variations:

  • withConnect(definition, connectorId) — bolt broker auth onto an existing connection definition without re-typing the auth field:

    import { withConnect } from '@stackbone/sdk/connect';
    export default withConnect(
      defineOpenAPIConnection({ spec: 'https://api.example.com/openapi.json' }),
      'example',
    );
  • connectHeaders(connectorId, headerName?) — for providers that authenticate with an API-key header instead of a Bearer token. A connection runtime's auth carries only a Bearer token; anything else rides the connection's separate headers closure. connectHeaders produces that closure, pre-wired to the broker, placing the minted token under headerName (defaults to X-Api-Key):

    import { connectHeaders } from '@stackbone/sdk/connect';
    export default defineOpenAPIConnection({
      spec: 'https://api.example.com/openapi.json',
      headers: connectHeaders('example', 'X-Api-Key'),
    });

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:

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):

try {
  await stackbone.connection('slack').call('chat.postMessage', { channel, text });
} catch (err) {
  const code = err instanceof Error ? (err as { code?: string }).code : undefined;
  if (code === 'app_not_installed') {
    // Operator hasn't installed this connector's credentials yet.
  }
}

Common codes: invalid_args, credential_error, no_token, timeout, app_not_installed, 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. Every connector call is HMAC-signed automatically and scoped to your install.
  • The broker resolves the credential server-side, runs the action, and returns only the output — the credential never enters your container.
  • Running outside a Stackbone runtime (no broker URL or install id on the process) throws an actionable error telling you to run the agent through stackbone dev.

That is the whole contract you depend on; the routes and 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.

Migrating from the bare connection(id) export

The bare top-level connection(id) export on @stackbone/sdk/workflow and @stackbone/sdk/connect is deprecated — it is an alias for stackbone.connection(id) and behaves identically. Prefer the namespaced form so every SDK capability reads as stackbone.<noun>.

Where to go next

The platform for agent developers.Build, ship and observe agentsthat survive prod.

© 2026 · STACKBONEBUILT WITH ❤️ FROM CANADA AND SPAIN