--- title: 'Integrations' description: 'Stackbone Connect: connect Gmail, Outlook, Telegram, WhatsApp, Linear, GitHub or any OpenAPI service once in Studio, call it from your code by name, and start a workflow when an event arrives.' position: 5 --- # Integrations > **Stackbone Connect** is how your agents and workflows reach the outside > world, in both directions. An operator connects a provider once in Studio, > and the credential stays in the **broker** inside your box. Your code names > the connector and an operation; the broker mints a short-lived token and > makes the call, so no key ever enters your container. When something happens > on the provider's side, a new email say, a **trigger** starts a durable > workflow run with it. | Direction | What happens | Where you set it up | | --------- | -------------------------------------------------------------------------- | ------------------------------------------------------------- | | Outbound | A tool or a workflow step calls a provider: send a message, read a thread. | **Connections** in Studio, then one line in your code. | | Inbound | A provider event starts a workflow as a durable run. | **Triggers** in Studio. No code, unless the mapping needs it. | ``` your code ──names connector + operation──▶ the broker, inside your box ├─ holds the real credential ├─ mints a short-lived scoped token └─ makes the call, returns the output ``` ## Connect a provider Open **Connections** under Integrations and click **Register integration**. The catalog lists the providers Stackbone knows, and a **Custom integration** for any other service that speaks OpenAPI. ![The "Add an integration" dialog in Studio: a Custom integration card and cards for Gmail, Outlook, Outlook (App-Only), Telegram and WhatsApp, each with its category and a Connect action.](/images/integrations/catalog.png) _The catalog. Custom integration covers any OpenAPI service you set up by hand._ Pick one and the technical fields come pre-filled from the catalog. You add the credential: the API key, or the OAuth client id and secret. It is **write-only**: the broker encrypts it and never reads it back to the screen. For a custom integration you also paste the provider's OpenAPI document (JSON or YAML), its base URL, the auth mode, the scopes and where the token travels. Registering the same integration again rotates its credential in place. ![The Connections screen listing two integrations: a Gmail one marked "No account" with a Connect account button, and a Telegram one marked "Ready".](/images/integrations/connections.png) _A key-based provider is ready as soon as you register it. An OAuth one waits for an account._ Each row says what still needs doing. A provider that authenticates with a key is **Ready** on registration. An OAuth provider shows **No account** until you click **Connect account** and finish the provider's sign-in; that grant is the account the broker acts as. Open **Operations** on a row to see what the provider exposes, read live from its OpenAPI document. Those operation ids are what your code names. ![The operations screen for the Gmail integration: a long list of Gmail API operations, each with its method, path and id, such as gmail.users.messages.send.](/images/integrations/operations.png) _What the Gmail integration exposes. The id under each path is what your code calls._ Registering, editing and removing an integration takes the `owner` or `admin` role: a connection holds a customer credential, so it follows the same gate as secrets. ## Call it from your code After you connect a provider, your code reaches it by name. There is no client to build and nothing to import from the provider. | You are in | Use | Import from | | ------------------------- | ----------------------------------------- | ------------------------- | | An agent's tool list | `connectorTool({ connector, operation })` | `@stackbone/sdk/deep` | | A tool body or a step | `stackbone.connection(id).call(op, args)` | `@stackbone/sdk` | | A workflow step, explicit | `callConnector(id, op, args)` | `@stackbone/sdk/workflow` | The shortest form gives an agent a provider operation as a tool. The model sees a tool named after the connector and the operation; the body runs through the broker: ```ts // deep-agents/support/index.ts import { z } from 'zod'; import { defineDeepAgent, connectorTool } from '@stackbone/sdk/deep'; export default defineDeepAgent({ name: 'support', model: 'openai/gpt-4o-mini', tools: [ connectorTool({ connector: 'telegram', operation: 'send-message', description: 'Send a Telegram message to a chat.', schema: z.object({ chat_id: z.string(), text: z.string() }), }), ], }); ``` The agent's own instruction — "reply to customers on Telegram" — is a prompt in the catalogue you write in Studio, not a string in this file. See [Prompts](/docs/sdk/platform/prompts). From a workflow step, call the operation directly: ```ts // workflows/notify.workflow.ts import { callConnector } from '@stackbone/sdk/workflow'; async function notify(chatId: string, text: string) { 'use step'; await callConnector('telegram', 'send-message', { chat_id: chatId, text }); } ``` While `stackbone dev` runs, it reads each connector's document and writes typed operation maps into `.stackbone/connect.d.ts`, so `stackbone.connection('gmail').call(...)` autocompletes its operations and their arguments. A call fails with a coded error rather than a token: for example `connector_installation_required` when the provider was never connected. The [`stackbone.connection`](/docs/sdk/platform/connections) page covers every call form, the principal a call is made as, and the error codes. ## Receive events A **trigger link** binds one provider event to one workflow. Open **Triggers** under Integrations and click **New link**: pick the integration (one whose catalog entry fires a trigger), the trigger, the workflow to start, and the credential to poll with, the shared account by default. A new link starts switched off, so nothing runs until you have mapped the event. Map the event onto the workflow's input. The event has its own fields (`from`, `subject`, `threadId`); the workflow declares its own input schema. The mapping editor puts them side by side. Click a field on the left, then the field it should fill, or drag between their dots. One event field can feed several inputs. **Transforms** (a regex, a template, a default, a date format, JSON parse, a free expression) sit between the two when a value needs shaping. **Auto-map** asks the workspace's model for a first draft, drawn dotted until you accept each line. **Code** shows the same mapping as one expression you can edit by hand. ![The trigger link detail in Studio: source gmail · message-received, workflow refund, credential app, status disabled, and the visual mapping with the event's subject wired to orderId and threadId wired to the correlation key.](/images/integrations/mapping.png) _Two lines drawn, one required field still to fill. The link stays off until it is complete._ The editor names any required input still unmapped. **Test against the sample event** runs the mapping over a representative event from the catalog and shows what the workflow would receive. **Save mapping** keeps the polling cursor, so events already processed are not replayed. Then **Turn on**. From then on the box polls the provider on the trigger's own interval (30 seconds by default) and starts one durable run per new event. Those runs sit in **Runs** with the others, and **View runs** on the link filters to them. The polling itself is one of the box's [recurring jobs](/docs/home/features/recurring-jobs), so its cadence and last result are on that screen too. ![The Triggers screen listing one Gmail link: message-received to the refund workflow, mapped, disabled, never polled, with the integration flagged as having no credential yet.](/images/integrations/triggers.png) _The Triggers list: each link with its state, its mapping and its last poll._ ### Resume a run instead of starting a new one By default every event starts a new run. A workflow that emailed someone and paused for their reply wants the reply to wake **that** run. Wire the event field that identifies the conversation, the `threadId` in the mapping above, to the **correlation key**, and have the paused workflow park its hook on that same value (see [waiting for a reply](/docs/sdk/humans/approval#wait-for-a-reply)). When an event arrives, a run parked on that key resumes with the state it built up, and the delivery says `resumed`; with no run parked, a new run starts and the delivery says `started`. If every delivery says `started` when you expected replies to continue a conversation, the two sides are not landing on the same value. ### Try the path without a real account Start the workflow the event would start, with your own input: ```sh stackbone workflows start refund --input '{ "orderId": "A-1042", "amount": 89.9 }' ``` ## How it stays safe - The broker runs inside your box. It encrypts credentials there, and they never reach the control plane or your agent code. - The runtime signs every call your code makes, and mints a token for that one call and that one connector. - A connector call outside the runtime (`stackbone dev` or a deployed box) fails with an error saying so, because nothing outside it can sign a call. - The box refreshes the token an OAuth provider handed over, on its own timer, another of the recurring jobs, so a long-lived trigger keeps polling. - A call made while the runtime is running your code carries the id of the run that made it. You pass nothing, and a connector call traces back to the run that issued it. See [`stackbone.connection`](/docs/sdk/platform/connections#what-the-runtime-provides). ## Read more - [`stackbone.connection`](/docs/sdk/platform/connections): every way to call a connector from code, principals, and the error codes. - [Human-in-the-loop](/docs/sdk/humans/approval#wait-for-a-reply): parking a workflow on a key so a reply resumes it. - [Recurring jobs](/docs/home/features/recurring-jobs): the polling and the token refresh, on the screen that lists every timer. - [`stackbone workflows start`](/docs/cli/reference/workflows#stackbone-workflows-start): start a workflow by hand with the input an event would carry.