Connectors
A Connector is a typed integration point with an external system — GitHub, GitLab, Gitea, Jira, Confluence, Slack, S3, an email provider, an LLM provider, anything an org needs to reach. Connectors are how Xema calls outward without giving agents (or workflows) raw credentials. The connector is the type; a ConnectorBinding is the per-org installation of that type with concrete credentials, scopes, and a target resource.
All inbound webhooks from external systems enter through one gateway. All outbound calls go through the same gateway with policy-mediated credential mint. There is no "just use the SDK directly" escape hatch — every external interaction is a capability call.
Two objects, one model
Connectors and bindings are first-class typed objects in Objects:
| Object kind | What it represents | Owner |
|---|---|---|
Connector | The integration type — GitHub, Slack, Jira, … | Contributed by a biome |
ConnectorBinding | One concrete installation — credentials + target | Owned by an Org or Project Space |
A biome contributes a connector by shipping a connector contribution in its manifest. An org admin (or, for project-scoped resources, a project admin) creates a binding by completing the install flow for that connector — picking the credential kind, supplying the secret, and selecting the target resource.
Bindings are addressable through the Space URI grammar:
1xema://orgs/acme/connector-binding/github-main
2xema://orgs/acme/projects/billing/connector-binding/jira-prodA workflow or agent that needs to call out names the binding URI — never the provider, never the token.
Why connectors and not "the agent calls the SDK"
Three reasons, all enforced at the gateway:
- Credentials never leave the gateway. The minted token is bound to one invocation, one capability, and a short TTL. Even a fully compromised agent process cannot leak a usable secret.
- Every external call is auditable as a capability invocation. The audit row records the binding ref, the capability ref, the subject, and the policy decision — not a free-form HTTP log.
- Provider swap is a binding change, not a code change. A workflow that opens a pull request names
connector:scm.create-pull-request@1. The same YAML works against GitHub, GitLab, or Gitea if the org's binding for that step points there.
Capabilities a connector exposes
A connector ships one or more capability refs under the connector: domain. An illustrative set is:
1connector:scm.create-pull-request@1
2connector:scm.merge@1
3connector:tracker.issue.create@1
4connector:docs.publish-page@1
5connector:channel.send-message@1
6connector:llm.invoke@1Each ref maps to a typed input schema, a typed output schema, and an explicit Execution Environment requirement. The connector's biome manifest declares which capabilities the connector exposes; the install flow surfaces them in the Permission Digest the admin approves.
Wallet credentials — the closed CredentialKind set
The gateway supports a fixed set of credential kinds. Each kind is a strategy: a pure projection from the encrypted at-rest payload to a freshly minted token the runner can use.
CredentialKind | What it carries | When to use |
|---|---|---|
OAuthUser | Three-legged OAuth user-delegated access + refresh token | Acting on behalf of a real human (GitHub user app, Atlassian OAuth) |
OAuthClient | Two-legged client_credentials grant token | Server-to-server with the connector's own client identity |
Pat | Personal access token, no expiry | CI-style integrations against GitHub/GitLab/Gitea |
ApiKey | Long-lived API key, no expiry | Slack bot tokens, Linear keys, third-party SaaS keys |
BotToken | Provider-issued bot token | Workspace-scoped Slack/Discord bots |
Basic | username + password | Legacy SCM hosts, internal endpoints |
RestrictedKey | Scoped API key (provider-side scopes encoded) | Stripe restricted keys, Twilio scoped tokens |
AppInstall | GitHub-App-style installation token | First-party app installations across many repos |
IamRole | Cloud IAM role assumption parameters | AWS/GCP/Azure access using STS or workload identity |
SignatureOnly | Webhook secret only — no minted credential | Inbound-only providers (verify, no outbound) |
ImapAuth / SmtpAuth | Email wallet | Email-provider connectors |
The enum lives in @xemahq/contracts (CredentialKind); its wire values are snake-case (oauth_user, app_install, signature_only, …).
Credential rotation is not performed on the mint hot path. Strategies are pure projections; rotation runs out-of-band on a schedule. A token whose expiresAt is past is refused with a typed denial — there is no silent fallback to a stale token.
OAuth installation flow
For OAuthUser and OAuthClient bindings, the install flow is uniform across providers:
- The admin opens the connector's install page (a biome-contributed UI surface).
- The gateway issues a one-time state token and redirects to the provider's authorize endpoint with the connector-declared scopes.
- The provider redirects back to the gateway callback. The gateway verifies the state, exchanges the code for tokens, and writes an encrypted credential payload under the new binding.
- The binding moves to
ready.
Declaring an onInstall path in a biome manifest does not currently execute it. A connector that needs an initial sync must run that work through a shipped service or an explicit workflow; see Lifecycle Hooks.
The redirect URL is always a gateway endpoint — never a biome endpoint. Biome code is never on the OAuth code-exchange path. Provider client secrets live in the gateway's KMS-backed secret store; biome manifests reference them by name, not value.
Inbound webhooks — one ingress edge
Every inbound webhook from an external provider enters the gateway through a single ingress edge. The gateway:
- Verifies the provider signature using the binding's
SignatureOnlypayload (or the bound credential's signing key). - Looks up the binding from the provider's
(provider, deliveryId, accountId)tuple. - Translates the raw payload into a canonical
ConnectorWebhookEnvelopeDto<TPayload>(@xemahq/platform-common) typed perWebhookEntityKind. - Forwards the envelope to the owning domain service with a deterministic
Idempotency-Key = {provider}:{deliveryId}.
Domain services receive canonical envelopes only — they never parse provider-specific payloads, never touch raw signatures, never see the binding's secret. Adding a new provider is a gateway-side change; downstream consumers are untouched as long as the canonical envelope is unchanged.
Connection status
ConnectorStatus is a closed enum with three members:
| Status | Meaning |
|---|---|
ACTIVE | Credentials present and validated; the gateway mints from this connection |
ERROR | The last mint, refresh, or webhook verification failed |
REVOKED | An admin or the provider revoked it; mint refuses, audit is retained |
A revoked connection is never silently restored. The admin re-runs the install flow to produce a new one.
Multiple accounts per provider
An organization routinely has more than one account with the same provider — two Slack workspaces, a personal GitHub identity alongside an org app install, a project-specific Jira. A connection therefore carries a name and a default flag, at a declared scope tier.
| Field | What it does |
|---|---|
name | Distinguishes connections to the same provider. Defaults to default |
isDefault | Exactly one connection per (owner, provider) is the default |
scopeTier | Which tier owns it — system, org, project, or user |
The invariant is N named connections per (owner, provider), exactly one default, and it is enforced by eight partial unique indexes in the database rather than by application code — one name-uniqueness index and one single-default index per tier. It is not possible to write two defaults, and it is not possible to write two connections with the same name at the same tier.
scopeTier is not a vocabulary of its own. It is CREDENTIAL_OWNER_SPACE_KINDS
(@xemahq/contracts) — an admissible subset of the platform's single
SpaceKind ownership vocabulary, naming the four tiers that may own a
credential. A connection tier therefore means exactly what the same tier means
everywhere else in Xema, and the narrowest-first order the resolver walks is
derived from SPACE_KIND_RANK rather than restated here.
Session is deliberately not in the subset: a credential must outlive the
session using it, or nobody can rotate or revoke it.
Two consequences of it being a subset rather than a copy. The platform tier is
spelled system, the same as everywhere else — an earlier revision of this page
called it platform, which was a third spelling of one concept and is retired.
And there was formerly a separate CredentialOwnerScope enum, named without
backticks here because it no longer exists; do not look for it.
Resolution — narrowest wins, and ambiguity is an error
The resolver walks the ladder user → project → org → platform and takes the default connection at the narrowest tier that has one.
What it never does is pick arbitrarily. If a tier has candidate connections but none of them is declared the default, resolution stops with a typed 409 CONNECTION_AMBIGUOUS — it does not fall through to a wider owner, because falling through is how a call silently reads the wrong mailbox. If no tier has a candidate at all, the answer is 404 CONNECTION_UNRESOLVED.
A caller that wants a specific account names its connection id; a caller that wants "the org's" names nothing and gets the default.
Who can see a connection
Listing is fenced by a query predicate, not only by a route guard, and the rule differs by tier:
org- andproject-tier connections are visible to any member of the org.user-tier connections are visible only to the user who owns them — including against an org admin.platform-tier connections never appear in an org listing at all.
Mutation paths resolve the connection through the same predicate, so a connection you cannot see is also one you cannot delete, rotate or make default.
First-party connectors
These connector biomes ship with Xema. Each contributes a connector-adapter through a biome manifest. An admin installs the biome, then creates a binding as described in Two objects, one model. Capability ids below are exactly as the manifests declare them.
Gmail
Package @xemahq/biomes-platform-gmail, connector key GMAIL. It ingests Gmail and Google Workspace mail. The adapter registers a users.watch watch on a mailbox so Gmail publishes change notifications to a Cloud Pub/Sub topic. The gateway's push handler resolves each notification's historyId through a history.list delta, normalizes every fetched message into the canonical mail webhook envelope, and forwards it through the connector gateway ingress edge. The next history.list delta recovers a missed push.
- Capability:
connector:mail.fetch-message@1(internal exposure). The connector requirement listsread.mail.messageandread.mail.attachment-metadata. - Credential:
oauth_user, through the Google OAuth flow. Scopes aregmail.readonlyandgmail.send. - Provider app setup (admin): the Google provider takes an OAuth client ID and client secret, a Pub/Sub topic (
projects/{project}/topics/{topic}), the Pub/Sub push OIDC audience, the push service account email, and an optional watch label id. - Binding setup (admin): choose the connected Google account, the mailbox to monitor (an address, or
mefor the consenting user's own mailbox), and an optional label. The label defaults toINBOX.
Microsoft 365
Package @xemahq/biomes-platform-ms365, connector key MICROSOFT_GRAPH. It ingests Outlook and Microsoft 365 mail. The adapter subscribes to Microsoft Graph change notifications on a mailbox, normalizes each fetched message into the canonical mail webhook envelope, and forwards it through the connector gateway ingress edge.
- Capability:
connector:mail.fetch-message@1(internal exposure). The connector requirement listsread.mail.messageandread.mail.attachment-metadata. - Credential:
oauth_user, through the Microsoft OAuth flow. Scopes areopenid,offline_access,Mail.Read,User.ReadandMail.Send. - Provider app setup (admin): an application (client) ID and client secret from one Azure AD (Entra ID) app registration, plus an optional tenant id. Blank or
commonmeans a multi-tenant registration. - Binding setup (admin): choose the connected Microsoft 365 tenant, the mailbox user id or UPN, and the folder to subscribe to:
inbox(default),archive,clutterorjunkemail.
HubSpot
Package @xemahq/biomes-platform-hubspot, connector key HUBSPOT_CRM. It gives agents and workflows read-only access to an org's HubSpot CRM contacts, companies and deals. Every operation is a declarative HTTP request template that the gateway runs against https://api.hubapi.com. The connector ships no webhook, so nothing is ingested.
| Capability | Request | Scope |
|---|---|---|
connector:hubspot.search-contacts@1 | POST /crm/v3/objects/contacts/search | crm.objects.contacts.read |
connector:hubspot.get-contact@1 | GET /crm/v3/objects/contacts/{contactId} | crm.objects.contacts.read |
connector:hubspot.search-companies@1 | POST /crm/v3/objects/companies/search | crm.objects.companies.read |
connector:hubspot.search-deals@1 | POST /crm/v3/objects/deals/search | crm.objects.deals.read |
- Credential:
api_key, a HubSpot private-app access token sent as a bearer header. The credential broker holds it at org scope and it is never shown to an agent. There is no shared platform app to set up. - Binding setup (admin): create a private app in HubSpot with only the three read scopes above, then supply its access token. One connection per HubSpot account.
IFS Cloud
Package @xemahq/biomes-platform-ifs, connector key IFS_CLOUD. It creates records in an org's own IFS Cloud projections (OData/REST) from workflows. The caller supplies the projection entity-set path and the record body, so any IFS entity can be written. It is outbound-only and has no inbound webhook.
- Capability:
connector:ifs-cloud.create-record@1. The connector requirement listswrite.record. - Credential:
basic, holding an OAuth2 client-credentials pair (client ID as the username, client secret as the password). - Binding setup (admin): supply the IFS Cloud API base URL (the projection base path), the OAuth token URL, the client ID and secret, and an optional OAuth scope.
IBM Domino (Lotus Notes)
Package @xemahq/biomes-platform-lotus-domino, connector key LOTUS_DOMINO. It creates documents in an org's own IBM Domino databases through the Domino REST API. The caller supplies the data source (database) and the document body. It is outbound-only and has no inbound webhook.
- Capability:
connector:lotus-domino.create-document@1. The connector requirement listswrite.document. - Credential:
basic, the Domino REST API username and password. - Binding setup (admin): supply the Domino REST API base URL, the username and the password.
AWS SES
Package @xemahq/biomes-platform-aws, connector key AWS_SES. It sends transactional email through Amazon Simple Email Service from workflows. It is outbound-only and has no inbound webhook. The biome is named for AWS in general so further AWS services can ship in the same package later; today only SES sending is shipped.
- Action: the workflow action
connector-aws/[email protected]. Inputs arefrom,to,subjectandtextBody(required), pluscc,bcc,replyTo(up to 50 addresses each),htmlBody,configurationSetNameand a per-sendregion. The output is the SESmessageId. Thefromaddress must be verified in SES or live under a verified domain. The action retries up to 3 times and treats invalid-argument and authentication failures as non-retryable. - Credential:
iam_role. The workflow supplies no key: Xema assumes the IAM role you nominate when the step runs and uses short-lived credentials for that one send. Nothing secret is placed in workflow inputs or run history. Version 1 of the action took access keys as inputs; version 2 removed them, so a workflow that still passes them fails validation. - Binding setup (admin): create an IAM role that trusts Xema's workload identity and allows
ses:SendEmail, then supply the role ARN, an optional default region, and an optional STS session name (it shows up in your AWS audit trail for each assumed-role call).
1jobs:
2 send-welcome:
3 uses: connector-aws/ses-[email protected]
4 with:
5 from: ${{ vars.AWS_SES_FROM_ADDRESS }}
6 to:
7 - ${{ inputs.recipient }}
8 subject: Welcome to Xema
9 textBody: Hi, welcome to Xema!IMAP
Package @xemahq/biomes-platform-imap, connector key IMAP_GENERIC. It reads mail from any IMAP server. Mail is polled, not pushed, so there is no webhook. The connector exposes four capabilities, each in the mail family:
| Capability | What it does |
|---|---|
connector:mail.fetch@1 | Fetches messages newer than a UID cursor you pass in, up to a limit (default 200), from a mailbox (default INBOX). Returns text and HTML bodies and, optionally, base64 attachments within a size limit, plus the new highest UID so the caller can store it. An optional expected UID validity makes a mailbox reset visible. |
connector:mail.fetch-by-uid@1 | Fetches one message by UID, with an option to mark it \Seen after reading. |
connector:mail.search@1 | Searches a mailbox by unseen, flagged, since, before, from, to, subject, body or uid, newest-first or oldest-first, up to a limit. |
connector:mail.mark@1 | Adds IMAP flags (default \Seen) to one message or a UID range. |
The connector does not keep the cursor between runs; the workflow or agent that calls fetch stores the returned UID. Messages and responses are size-capped, and a message over the cap is reported as refused rather than truncated.
- Credential:
imap_auth. - Binding setup (admin): IMAP host, port, username, password, and an optional TLS mode (
auto,tls,starttlsorplain). Withauto, port 993 uses implicit TLS. Each mailbox is its own binding; there is no shared platform app.
SMTP
Package @xemahq/biomes-platform-smtp, connector key SMTP_GENERIC. It sends mail through any SMTP server. It is outbound-only.
| Capability | What it does |
|---|---|
connector:mail.send@1 | Sends a message: to (required), subject, cc, bcc, replyTo, from, a text and/or HTML body, extra headers and attachments (base64 content). |
connector:mail.send-reply@1 | Sends a reply that carries inReplyTo and references so mail clients thread it with the original message. |
- Credential:
smtp_auth. - Binding setup (admin): SMTP host, port, username, password, an optional TLS mode (
auto,tls,starttlsorplain; withauto, port 465 uses implicit TLS and 587 uses STARTTLS), and an optional default sender address and name used when a send names nofrom.
Stripe
Package @xemahq/biomes-platform-stripe, connector key STRIPE. It lets an org receive events from its own Stripe account inside its own workflows and apps. It is inbound-only and declares no outbound capabilities.
This is unrelated to Xema's own billing. The connector holds the org's Stripe key at org scope; installing it does not depend on, and does not affect, how Xema charges for credits or subscriptions.
The connector maps a deliberately small set of Stripe events. Anything else is received, recorded and dropped, and no run starts.
| Stripe event | Canonical event |
|---|---|
payment_intent.succeeded | payment.intent.succeeded |
payment_intent.payment_failed | payment.intent.failed |
checkout.session.completed | payment.checkout.completed |
charge.refunded | payment.charge.refunded |
invoice.paid | payment.invoice.paid |
Each delivery is verified against the Stripe-Signature header using the endpoint signing secret, and a request whose timestamp is more than 5 minutes off is rejected.
- Credential:
restricted_key, a Stripe restricted key. - Binding setup (admin): create a restricted key in Stripe with only the access you need, add a Stripe webhook endpoint that points at the Xema ingress for Stripe, and supply the restricted key and the endpoint's signing secret (
whsec_...). One connection per Stripe account.
Telegram
Package @xemahq/biomes-platform-telegram, connector key TELEGRAM. It connects a Telegram bot to conversation channels, so a message sent to the bot reaches an agent session and the agent's reply goes back to the chat.
- Inbound: text messages and edits to text messages from the Bot API. Each becomes a canonical
channel.message.receivedorchannel.message.editedevent carrying the sender id, chat id, message id, text and, for forum topics, the topic as the thread id. Other updates (channel posts, callback queries, polls, stickers and media) are recorded and dropped. - Verification: Telegram echoes the secret token you set when registering the webhook in the
X-Telegram-Bot-Api-Secret-Tokenheader, and Xema compares it. The secret token is stored on the channel binding, not in the credential. - Outbound:
connector:channel.send-message@1withconversationId(the chat id) andblock(one coalesced reply). Telegram limits a message to 4096 characters, so a longer block is split into parts sent in order; sending stops at the first failure and reports how many parts went out. - Credential:
bot_token. - Binding setup (admin): create a bot with BotFather, supply its bot token, register its webhook with a secret token, and pick the Telegram bot connection when installing the connector.
Slack provides the same connector:channel.send-message@1 capability for Slack workspaces; see the Slack section of the integration guide.
These connectors terminate at the connector gateway and ship no route of their own. Inbound pushes and webhooks enter only through the gateway's single ingress edge.
Source control, issue tracker, documentation and Slack connectors (GitHub, GitLab, Gitea, Azure DevOps, Atlassian, Linear, Slack) are described, with their events and binding setup, in the Integration Guide.
Connector sync
Connector sync keeps a project's knowledge base and backlog in step with the tracker and documentation tools bound to that project, and moves a project's data between providers. It builds on the project's connector bindings, so it works with any tracker or documentation connector, such as the Atlassian connector.
Sync always acts under authority a person grants. It never uses a standing service identity.
Sync configurations
A sync configuration belongs to one project and one adapter kind, DOCUMENTATION (knowledge-base pages and spaces against a documentation tool) or TRACKER (backlog items against an issue tracker).
| Setting | Values |
|---|---|
direction | PUSH, PULL or BIDIRECTIONAL (default) |
conflictStrategy | INTERNAL_WINS (default), EXTERNAL_WINS, NEWEST_WINS or MANUAL |
syncIntervalMs | How often the schedule runs. Default 5 minutes, minimum 1 minute. |
enabled, paused | Turn the schedule off, or hold it without losing its settings. |
A new configuration does not run until someone authorises it. Authorising lends the schedule a durable delegation of that person's authority over the connection and the project; revoking it stops the schedule until it is authorised again, and removing a configuration revokes its authority first. Changing direction changes what the schedule is allowed to do, so a held authorisation that does not cover the new direction is revoked and you authorise again.
The authority on a configuration is in one of these states: not_required, required, unprovisioned, held or ended. If a run is refused because the person who authorised it no longer has the right it needs, the configuration shows a status line naming the owner, the missing right and the scope. That is a denial, so the next run can succeed once the right is restored. If the delegation has ended, the run fails and the configuration needs authorising again.
Mapping a documentation space
Before the first documentation sync, a scan reads the provider's content structure and a person confirms how it lines up with the knowledge base.
- Analyse. Start a scan for the project and the connection its documentation binding uses. The scan runs under a delegation the caller authorises for that run, and the provider's tree is read live and never stored. An analysis is
SCANNING,COMPLETEDorFAILED. - Generate a mapping. Xema proposes a mapping between external resources and internal knowledge-base resources, and lists any resources it could not map on its own. You can pass project context (name, description, tags) to help.
- Confirm. Send the mappings you accept, each as an external id, an internal id and an item type, and the external resource ids to exclude. Confirming creates the sync mappings.
Running a sync
You can run a sync now, for one adapter kind and connection, under a delegation you authorise for that run. The result is RUNNING, COMPLETED, PARTIAL or FAILED, with counts of items processed, failed and skipped. A project's sync status lists each configuration with its authority and its latest run (triggered MANUAL or SCHEDULED), plus mapping counts by sync status.
Migrations
A migration moves a project from one provider to another, for example from the internal tracker to an external one.
| Setting | Values |
|---|---|
mode | switch_only (default), migrate_and_switch or clone_and_sync |
direction | export (default), import or merge |
Creating a plan scans the source and moves no data. The plan then waits for a human decision, and the decision names what will move. A migration job passes through PLANNING, PENDING_CONFIRMATION, CONFIRMED, RUNNING and COMPLETED, or ends as FAILED or CANCELLED. You can cancel a plan before it executes. The status of a job includes the open decision, so the person who raised the plan can see what it is waiting on. They cannot answer it themselves.
Related concepts
- Capabilities — connectors expose capability refs; the connector pilot section shows the end-to-end flow.
- Spaces — bindings live in an Org or Project Space and inherit its data-classification floor.
- Policy — every connector capability call passes through a policy decision.
- Execution Environments — connector capabilities declare the environment they run under (e.g.
projectfor project-scoped SCM). - Biomes — connectors are contributed by biomes through the
connectorcontribution kind.
Previous: ← Capabilities Next: Conversation channels →