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 outputConnect a provider
Open Connections under Integrations and click Register connector. The catalog lists the providers Stackbone knows, and a Custom integration for any other service that speaks OpenAPI.
The catalog. Custom integration covers any OpenAPI service you set up by hand.
What ships in the catalog
Nine connectors come with the platform. Their protocol, their authentication and their events are already declared, so you only add the credential.
| Connector | Authentication | Starts a workflow when |
|---|---|---|
| Gmail | OAuth | A message arrives in the inbox |
| Outlook | OAuth | A message arrives in the Inbox |
| Outlook (App-Only) | Client credentials | A message arrives in a named mailbox |
| Google Drive | OAuth | A file is added to a folder |
| GitHub | API key | A commit is pushed, an issue is updated |
| Linear | API key | An issue is created, a comment is added |
| Supabase | API key | A row is inserted |
| Telegram | API key | Never. This one only sends |
| API key | Never. This one only sends |
Anything else is a Custom integration: you point it at the provider's OpenAPI document and it gets the same treatment, operations included. The list grows, so the dialog in Studio is the current answer, not this table.
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.
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. Your code names those operation ids.
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:
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 it in Studio, not in this file. See Prompts.
From a workflow step, call the operation directly:
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 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 trigger: pick the connector (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.
Some triggers need one more thing from you: a value that scopes the watch to a place. Google Drive's folder trigger asks for a Folder ID, the last segment of the folder's URL; Outlook (App-Only) asks for a mailbox. The dialog labels and explains whichever value the trigger declares, and the link cannot be armed without it.
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.
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 the box does not replay events it already handled. Then Turn on.
From then on the box polls the provider and starts one durable run per new event. Every link polls on the same cadence, 30 seconds by default. 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, so its cadence and last result are on that screen too.
The Triggers list: each link with its state, its mapping and its last poll.
What counts as a new event
A poll asks the provider what changed since last time, so what starts a run is whatever the provider's own dates say moved. Google Drive's File added to folder fires when a file arrives in the folder, and also when a file the link has never delivered is edited — a document that was already sitting there when you armed the link, for instance.
It fires once per file, though. Once a file id has been delivered, later edits to it start no further run.
The event carries both of Drive's dates, so the workflow can tell which of the two happened. Read the gap between them, not just their order:
| What you see | What happened |
|---|---|
modifiedTime before createdTime |
Someone uploaded a document written earlier. Drive keeps the source file's own date. |
| The two within a few seconds | The file was created in the folder. A workflow that files through this connector lands here: it writes the name and the parent first, then the bytes, so modifiedTime settles a moment after createdTime. |
modifiedTime clearly later |
The file was edited where it already sat. |
Delivery is at-least-once per item, keyed on the provider's own id — for Drive, the file id. Three consequences worth knowing before you design the workflow:
- A document dropped in the folder starts one run, however old the document is. Drive keeps the source file's own modified date on upload, so a contract last edited in March arrives stamped March. The trigger asks on the arrival date as well, which is what makes it fire anyway.
- Editing that same file again does not start a second run. The delivery is already claimed under that file id. A re-uploaded recording does start one, because a re-upload is a new file in Drive with a new id — not a new date on the old one.
- A file your own workflow writes into the folder it watches is an arrival like any other, so it fires the trigger and the run writes again. Watch one folder and write to a different one, or filter your own output out by name or mime type.
A Google Doc in a folder people work in is chatty for the same reason: its modified date moves on every collaborator's keystroke. That costs listings, not runs — every one of those polls dedups to the same file id.
Resume a run instead of starting a new one
By default every event starts a new run. When a workflow emailed someone and
paused for their reply, you want that 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).
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:
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 devor 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 it still traces back to that run. See
stackbone.connection.
Read more
stackbone.connection: every way to call a connector from code, principals, and the error codes.- Human-in-the-loop: parking a workflow on a key so a reply resumes it.
- Recurring jobs: the polling and the token refresh, on the screen that lists every timer.
stackbone workflows start: start a workflow by hand with the input an event would carry.