Integrations for coding agents
This page is the working reference for coding agents that build or use organisation-built integrations. The hosted OpenCloud Agent loads the same workflow from its organisation-integrations skill. For the product model and the dashboard, read the guide; for manifest fields, read the manifest reference.
Choose the approach
| The app needs… | Do this |
|---|---|
| A system another app in the organisation already publishes | Declare a provider: custom slot and call its operations. |
| A reusable connection to a system for several apps | Build a provider app that publishes provides.integration. |
| Google Workspace, Slack, HubSpot, Asana, or another built-in provider | Use the built-in slot instead. |
Find what the organisation publishes before building anything:
opencloud integration custom-list <app-id>MCP clients call list_custom_integrations. Integration names are unique in the organisation.
What stays with the person
Agents never ask for, read, print, log, store, or return credential values, OAuth tokens, or webhook URLs. These steps are always human actions in OpenCloud:
- entering credential values or signing in with OAuth under Integrations → Organisation integrations;
- sharing a provider app with the organisation (App → Sharing, Use audience);
- registering the OAuth redirect URI or a connection's webhook URL with the external system;
- binding a
calling_userslot to their own account.
An agent may bind an existing eligible connection to an app-account slot when the user asked it to.
Commands
Public CLI 3.11.0 or later provides these commands. The hosted MCP tools work with any client.
| Task | CLI | MCP |
|---|---|---|
| List published contracts | opencloud integration custom-list <app-id> | list_custom_integrations |
| Show slots, bindings, eligible connections | opencloud integration list <app-id> | None; the person uses App → Integrations |
| Bind an app-account slot | opencloud integration bind <app-id> <slot> --connection-id <id> --idempotency-key <key> | None; the person binds in App → Integrations |
| Development modes and test connection | opencloud app dev integration list [dir] | get_dev_integrations |
| Use the real system for one dev slot | opencloud app dev integration mode <dir> <slot> live|fake | set_dev_integration_mode |
| Choose a provider's dev test connection | opencloud app dev integration test-connection <dir> <connection-id> | set_dev_integration_test_connection |
| Send a synthetic event to a dev handler | opencloud app dev integration inject <dir> <slot> --type <type> | inject_dev_integration_event |
| Inspect production event deliveries | opencloud integration events <app-id> | list_integration_event_deliveries |
Give every mutating command an explicit idempotency key and reuse it only when retrying the same intended effect.
Build a provider
- Declare the contract in a schema-3 manifest. Every Function it names is
access: system. Pin SDK 2.6.0 when the contract hassync,webhook, orevents; operations alone work from 2.2.0. - Give each operation a realistic, deterministic
fakeoutput (at most 16 KiB). Consumers develop against it. - Partition stored data by connection with
connection_id uuid not null default auth.connection_id()and an RLS policyusing (auth.is_system() and connection_id = auth.connection_id()) with check (…)that covers all commands. - Deploy once so the integration appears in the catalog, ask the user to connect a sandbox or read-only account, select it as the development test connection, and invoke each Function with realistic input. Verification does not require invoking contract Functions, so never add development-only branches or hard-coded data to them.
- Tell the user what remains: sharing the provider app, connecting accounts, and registering redirect or webhook URLs.
Function contracts
| Function | Input | Returns | Can emit events |
|---|---|---|---|
| Operation | The consumer's input | JSON (at most 1 MiB, 25 s) or a typed error | Yes |
sync | {} on the cron schedule, per bound connection | Anything; runs up to 15 minutes | Yes |
webhook | { method, headers, body, receivedAt }; body is the exact request text (at most 256 KiB) | The response for the sender | Yes |
OAuth exchange | { code, redirectUri, codeVerifier } | { accessToken, refreshToken?, expiresIn? } | No |
OAuth refresh | { refreshToken } | { accessToken, refreshToken?, expiresIn? } | No |
Operation, sync, and webhook Functions read credential values with secrets.require(NAME) and an OAuth access token with secrets.require("OAUTH_ACCESS_TOKEN"). On SDK 2.6.0 their context has integration with connectionId, trigger, operation, consumerAppId, and emit({ type, id, data }).
- Typed 4xx errors reach the consumer with their code. Throw
errors.unauthorizedonly when the external system rejects the connection's credentials or token: the connection becomes Reconnect required and consumers receiveINTEGRATION_RECONNECT_REQUIRED. - Webhook Functions verify the sender's signature over
bodybefore parsing, with Web Crypto (crypto.subtle): Functions run on Deno and have no globalBuffer. 2xx results go back to the sender, 401 and 403 pass through, other 4xx become 400, and failures become 503 so the sender retries. - Event
idis the external system's stable ID: each subscribed binding records it once. - OAuth exchange posts the code, redirect URI, verifier, client ID, and the client secret from an app
secretsentry to the provider's token endpoint. Ask the owner to set that secret withopencloud secret configure.
Use an integration
integrations:
crm:
provider: custom
integration: acme-crm
account: app
cardinality: one
capabilities: [contacts.read]
events: { function: on-contact, types: [contact.created] }const { contacts } = await integrations.use("crm").call("contacts.list", {});- Declare only the capabilities you call. With
cardinality: many, pass{ bindingId }as the third argument. - A
calling_userslot acts with the signed-in person's own connection. OnINTEGRATION_CONNECTION_REQUIRED, showerror.details.connectUrl(SDK 2.6.0) as a "Connect" link. Development always returnsfakedata for these slots. - An event handler is a
systemFunction that receives{ id, type, occurredAt, integration, provider, bindingId, data }at least once. Store byid, and give its table a policy for all commands such asusing (auth.is_system()) with check (auth.is_system()):createreturns the new row, so an insert-only policy fails with 403.
In development, every operation returns its fake output until you switch an app-account slot to live. Test event handlers by injecting synthetic events and reusing the idempotency key to prove a replay is harmless. After deploying, bind a connection the user already created, then check opencloud integration events <app-id> and the Function logs.
Errors
| Code | Action |
|---|---|
INTEGRATION_PROVIDER_UNAVAILABLE | Check the published name with custom-list; the provider may be undeployed or briefly unavailable. |
CUSTOM_INTEGRATION_ACCESS_DENIED | The provider app is not shared with this person. |
INTEGRATION_CONNECTION_REQUIRED | Bind a connection (app slot) or show connectUrl (calling_user). |
INTEGRATION_RECONNECT_REQUIRED | The external system rejected the connection; its manager replaces the credentials or signs in again (calling_user: show connectUrl). |
INTEGRATION_CAPABILITY_DENIED, INTEGRATION_GRANT_DENIED | Declare the capability and bind again. |
INTEGRATION_BINDING_REQUIRED | Pass a bindingId. |
INTEGRATION_EVENT_NOT_DECLARED | Emit or inject only declared event types. |
INTEGRATION_TEST_CONNECTION_REQUIRED | Select a development test connection before invoking a contract Function. |
INTEGRATION_OAUTH_FUNCTION_NOT_INVOCABLE | OAuth exchange and refresh never run in development; connect an account instead. |
INTEGRATION_NAME_TAKEN | Choose another integration name. |
| Event handler 403 | The system identity cannot write or read back its row. |
