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 kindWhat it representsOwner
ConnectorThe integration type — GitHub, Slack, Jira, …Contributed by a biome
ConnectorBindingOne concrete installation — credentials + targetOwned 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:

text
1xema://orgs/acme/connector-binding/github-main
2xema://orgs/acme/projects/billing/connector-binding/jira-prod

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

  1. 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.
  2. 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.
  3. 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:

text
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@1

Each 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.

CredentialKindWhat it carriesWhen to use
OAuthUserThree-legged OAuth user-delegated access + refresh tokenActing on behalf of a real human (GitHub user app, Atlassian OAuth)
OAuthClientTwo-legged client_credentials grant tokenServer-to-server with the connector's own client identity
PatPersonal access token, no expiryCI-style integrations against GitHub/GitLab/Gitea
ApiKeyLong-lived API key, no expirySlack bot tokens, Linear keys, third-party SaaS keys
BotTokenProvider-issued bot tokenWorkspace-scoped Slack/Discord bots
Basicusername + passwordLegacy SCM hosts, internal endpoints
RestrictedKeyScoped API key (provider-side scopes encoded)Stripe restricted keys, Twilio scoped tokens
AppInstallGitHub-App-style installation tokenFirst-party app installations across many repos
IamRoleCloud IAM role assumption parametersAWS/GCP/Azure access using STS or workload identity
SignatureOnlyWebhook secret only — no minted credentialInbound-only providers (verify, no outbound)
ImapAuth / SmtpAuthEmail walletEmail-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:

  1. The admin opens the connector's install page (a biome-contributed UI surface).
  2. The gateway issues a one-time state token and redirects to the provider's authorize endpoint with the connector-declared scopes.
  3. 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.
  4. 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:

  1. Verifies the provider signature using the binding's SignatureOnly payload (or the bound credential's signing key).
  2. Looks up the binding from the provider's (provider, deliveryId, accountId) tuple.
  3. Translates the raw payload into a canonical ConnectorWebhookEnvelopeDto<TPayload> (@xemahq/platform-common) typed per WebhookEntityKind.
  4. 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:

StatusMeaning
ACTIVECredentials present and validated; the gateway mints from this connection
ERRORThe last mint, refresh, or webhook verification failed
REVOKEDAn 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.

FieldWhat it does
nameDistinguishes connections to the same provider. Defaults to default
isDefaultExactly one connection per (owner, provider) is the default
scopeTierWhich 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- and project-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 lists read.mail.message and read.mail.attachment-metadata.
  • Credential: oauth_user, through the Google OAuth flow. Scopes are gmail.readonly and gmail.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 me for the consenting user's own mailbox), and an optional label. The label defaults to INBOX.

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 lists read.mail.message and read.mail.attachment-metadata.
  • Credential: oauth_user, through the Microsoft OAuth flow. Scopes are openid, offline_access, Mail.Read, User.Read and Mail.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 common means 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, clutter or junkemail.

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.

CapabilityRequestScope
connector:hubspot.search-contacts@1POST /crm/v3/objects/contacts/searchcrm.objects.contacts.read
connector:hubspot.get-contact@1GET /crm/v3/objects/contacts/{contactId}crm.objects.contacts.read
connector:hubspot.search-companies@1POST /crm/v3/objects/companies/searchcrm.objects.companies.read
connector:hubspot.search-deals@1POST /crm/v3/objects/deals/searchcrm.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 lists write.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 lists write.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 are from, to, subject and textBody (required), plus cc, bcc, replyTo (up to 50 addresses each), htmlBody, configurationSetName and a per-send region. The output is the SES messageId. The from address 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).
yaml
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:

CapabilityWhat it does
connector:mail.fetch@1Fetches 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@1Fetches one message by UID, with an option to mark it \Seen after reading.
connector:mail.search@1Searches 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@1Adds 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, starttls or plain). With auto, 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.

CapabilityWhat it does
connector:mail.send@1Sends 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@1Sends 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, starttls or plain; with auto, port 465 uses implicit TLS and 587 uses STARTTLS), and an optional default sender address and name used when a send names no from.

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 eventCanonical event
payment_intent.succeededpayment.intent.succeeded
payment_intent.payment_failedpayment.intent.failed
checkout.session.completedpayment.checkout.completed
charge.refundedpayment.charge.refunded
invoice.paidpayment.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.received or channel.message.edited event 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-Token header, and Xema compares it. The secret token is stored on the channel binding, not in the credential.
  • Outbound: connector:channel.send-message@1 with conversationId (the chat id) and block (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).

SettingValues
directionPUSH, PULL or BIDIRECTIONAL (default)
conflictStrategyINTERNAL_WINS (default), EXTERNAL_WINS, NEWEST_WINS or MANUAL
syncIntervalMsHow often the schedule runs. Default 5 minutes, minimum 1 minute.
enabled, pausedTurn 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.

  1. 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, COMPLETED or FAILED.
  2. 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.
  3. 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.

SettingValues
modeswitch_only (default), migrate_and_switch or clone_and_sync
directionexport (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.


  • 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. project for project-scoped SCM).
  • Biomes — connectors are contributed by biomes through the connector contribution kind.

Previous: ← Capabilities Next: Conversation channels →