@nightowlsdev/integration-composio
Adapter/ToolingThe Composio IntegrationProvider backend, the first catalog-owning connector backend: 1000+ apps' tools and triggers come from Composio, which also runs the tool and holds the credentials.
What it does
@nightowlsdev/integration-composio implements the `IntegrationProvider` seam from `@nightowlsdev/connectors` against Composio, and it is the first backend that declares `ownsCatalog: true`: the tools and triggers are Composio's typed catalog rather than your own `defineConnector` definitions, and Composio executes the call and stores the user's credentials in its cloud. That makes it an optional, managed convenience, never the OSS default, the open defaults stay `@nightowlsdev/integration-native` (self-run OAuth, credentials on your infrastructure) or self-hosted Nango. The Composio SDK is INJECTED as a narrow `ComposioClient` seam, so the package carries no vendor dependency, stays engine-wall clean, and is hermetically unit-testable (the host wires the real `@composio/core`). `listTools` scopes the catalog to the connection's own app by default so a call never dumps thousands of tools into model context, then applies your optional `allow` predicate. `executeTool` maps failures onto the connector error model, a dead credential becomes a `ConnectorConnectionError` (re-auth), a transient or 429 becomes a `ConnectorRetryableError` (back off), anything else becomes a `ToolResult.error`, and success data is fenced before it reaches model context, because output from tools you did not author needs fencing more, not less. `startConnect` / `resolveConnection` / `revokeConnection` / `refreshStatus` map onto Composio's connection lifecycle; `handleInboundWebhook` appears only when you inject `parseTriggerEvent` (Composio is a ROUTED backend, so the host verifies Composio's own webhook signature at the HTTP layer first, then this parses the envelope for `dispatchTriggerEvent`), and `subscribeTrigger` appears only when the injected client can `enableTrigger`. To offer Composio for some apps and the open backends for others, compose them with `compositeProvider` rather than merging by hand.
Install
pnpm add @nightowlsdev/integration-composioKey exports
- composioProvider
- types: ComposioProviderOpts, ComposioClient, ComposioToolSpec, ComposioListToolsInput, ComposioExecuteInput, ComposioExecuteResult, ComposioConnection
Usage
import { composioProvider, type ComposioClient } from "@nightowlsdev/integration-composio";
import { compositeProvider, materializeIntegration } from "@nightowlsdev/connectors";
import { Composio } from "@composio/core"; // the SDK is INJECTED — this package has no vendor dep
// Wire the SDK to the narrow ComposioClient seam (listTools/executeTool/connections) — the
// package never imports the vendor, so the adapter is yours and the SDK can move under you.
const sdk = new Composio({ apiKey: process.env.COMPOSIO_API_KEY });
const client: ComposioClient = {
listTools: async (q) => toToolPage(await sdk.tools.get(q.entityId, { apps: q.apps, cursor: q.cursor })),
// connectionId PINS execution to the resolved connected account — forward it, never execute against "any".
executeTool: (q) => sdk.tools.execute(q.toolName, { userId: q.entityId, connectedAccountId: q.connectionId, arguments: q.args }),
initiateConnection: (q) => sdk.connectedAccounts.initiate({ userId: q.entityId, appName: q.app }),
getConnection: (q) => findActive(sdk.connectedAccounts, q),
disconnect: (q) => revoke(sdk.connectedAccounts, q),
};
// Composio owns the catalog (ownsCatalog: true): its 1000+ apps' tools, its execution, its credentials.
const composio = composioProvider({
composio: client,
entityIdFor: (ref) => `${ref.tenantId}:${ref.userId ?? "org"}`,
allow: (t) => !t.name.endsWith("_DELETE"), // optional, on top of the per-connection scoping
parseTriggerEvent, // optional: enables handleInboundWebhook
});
// Managed convenience, not the OSS default — route the sensitive apps to an open backend instead.
const provider = compositeProvider({
backends: { native: nativeProvider(/* … */), composio },
routeNew: (p) => (SENSITIVE.has(p) ? "native" : "composio"),
lookupConnectionBackend,
});
const swarm = defineSwarm({ agents, connectorTools: materializeIntegration(provider, ctxToRefs) });What it provides
integration-composio is the Composio IntegrationProvider adapter — the first CATALOG backend (ownsCatalog: true). Tools and triggers come from Composio's typed catalog (1000+ apps) rather than your own defineConnector defs, and Composio runs the tool and stores the user's credentials in ITS cloud (selfHostedCredentials: false). The Composio SDK is INJECTED as a narrow ComposioClient seam, so the package carries no vendor dependency, stays engine-wall clean, and is hermetically unit-testable — the host wires the real @composio/core. An optional, managed convenience — never the OSS default.
When to use it
- You want a large pre-built catalog (1000+ apps) without hand-authoring each provider's actions, and you're comfortable with Composio running the call and holding the tokens.
- You want managed OAuth + hosted credentials — no vault, no OAuth apps, no secret rotation to run yourself.
- You're offering the safe / commodity apps over Composio while keeping the sensitive ones on an open backend (compose via compositeProvider).
When not to
- You need your users' tokens on your own infrastructure — Composio stores them in its cloud. Use integration-native (or route the sensitive providers there).
- You need proxy or provider-direct webhooks — Composio declares proxy: false and directWebhooks: false; it is a routed backend, so the host verifies Composio's own webhook signature and this only parses the envelope.
- You want your gate to reason about the tool's real behavior — catalog tools default to needsApproval: true (side-effecting unless told otherwise) and their output is untrusted third-party data.
Alternatives
- @nightowlsdev/integration-nativeThe open default: self-run OAuth, credentials on your infrastructure, hand-authored defineConnector defs. Prefer it for any provider whose tokens you can't hand to a vendor.
- compositeProvider (from @nightowlsdev/connectors)You don't want to choose one backend globally — route each provider to native or composio, and let the composite serve an existing connection from whichever backend already owns it.
Strengths
- 1000+ apps out of the box — no per-provider connector authoring, no OAuth apps to register, no vault to run.
- No vendor lock at the code level: the SDK is injected behind ComposioClient, so the package imports nothing from @composio/core and the adapter is yours to move under.
- listTools is scoped to the connection's own app by default (never dumps thousands of tools into model context) and fails closed if no valid scope remains, with an optional allow predicate on top.
- Same P1 error model as native: dead credential → ConnectorConnectionError (re-auth), transient / 429 → ConnectorRetryableError (back off), else ToolResult.error; success data is fenced before it reaches model context.
- Catalog tools default to needsApproval: true, so the fail-closed gate treats them as side-effecting unless the spec says otherwise.
Limits & trade-offs
- Your users' credentials live in Composio's cloud (selfHostedCredentials: false) and calls are usage-metered — a closed, external dependency you chose deliberately, not the OSS default.
- Reduced governance surface: no proxy, no directWebhooks. Because the entity STRING is the identity, the encoder must fold the full resolver identity (use the exported composioEntityId) or two agents / orgScopes share one entity.
- A per-agent entity only RESOLVES at execute time — it cannot be established via startConnect (tenant-level) nor used for trigger registration (subscribeTrigger).
- Tool output is authored by tools you didn't write, so fencing matters more, not less; a catalog tool's inputSchema is a JSON Schema, not zod.
- handleInboundWebhook and subscribeTrigger only appear when you inject parseTriggerEvent / an enableTrigger-capable client — otherwise they're absent from the provider.
How it works
composioProvider(opts) implements the connectors IntegrationProvider seam over an injected ComposioClient. listTools calls the client scoped to the connection's app (fail-closed: it refuses to run unscoped, which could request the whole catalog), maps each catalog ComposioToolSpec to a ToolSpec (needsApproval defaults true), then applies your optional allow. executeTool validates the composio handle fail-closed, PINS execution to the resolved connected account (connectionId, not just the entity — an entity can hold several accounts for one app), classifies failures through the P1 model, and fences success data. startConnect / resolveConnection / revokeConnection / refreshStatus map onto Composio's connection lifecycle. Because Composio is a ROUTED backend, the host verifies Composio's webhook signature at the HTTP layer first, then handleInboundWebhook (exposed only when parseTriggerEvent is injected) parses the forwarded envelope for dispatchTriggerEvent.
Examples
Inject the SDK behind the ComposioClient seam
composioEntityId is the canonical encoder — hand-concatenating tenant and user ids is collision-prone and omits the agent + orgScope axes that per-agent isolation needs.
import { composioProvider, composioEntityId, type ComposioClient } from "@nightowlsdev/integration-composio";
// The package never imports @composio/core — you adapt the SDK to the narrow seam:
const client: ComposioClient = {
listTools: async (q) => toToolPage(await sdk.tools.get(q.entityId, { apps: q.apps, cursor: q.cursor })),
executeTool: (q) => sdk.tools.execute(q.toolName, { userId: q.entityId, connectedAccountId: q.connectionId, arguments: q.args }),
initiateConnection: (q) => sdk.connectedAccounts.initiate({ userId: q.entityId, appName: q.app }),
getConnection: (q) => findActive(sdk.connectedAccounts, q),
disconnect: (q) => revoke(sdk.connectedAccounts, q),
};
const provider = composioProvider({
composio: client,
entityIdFor: composioEntityId, // collision-safe: folds tenant+user+orgScope+callerSlug
allow: (t) => !t.name.endsWith("_DELETE"), // optional, on top of the per-connection scoping
});Compose Composio with the open backend
New connections route by policy; an existing connection is served by whichever backend owns it — never re-routed.
import { compositeProvider, materializeIntegration } from "@nightowlsdev/connectors";
import { nativeProvider } from "@nightowlsdev/integration-native";
import { defineSwarm } from "@nightowlsdev/core";
const SENSITIVE = new Set(["github", "stripe"]);
const provider = compositeProvider({
backends: { native: nativeProvider(/* … */), composio },
routeNew: (p) => (SENSITIVE.has(p) ? "native" : "composio"),
lookupConnectionBackend, // read an existing connection's owning backend from owl_connections
});
const swarm = defineSwarm({ agents, connectorTools: materializeIntegration(provider, ctxToRefs) });Parse a routed trigger envelope
Composio is a routed backend: the host verifies Composio's own webhook signature at the HTTP layer FIRST, then this parses the envelope.
const provider = composioProvider({
composio: client,
entityIdFor: composioEntityId,
parseTriggerEvent, // omit and handleInboundWebhook is NOT exposed
});
// after your HTTP-layer signature check on Composio's forwarded webhook:
const event = await provider.handleInboundWebhook?.(rawBody, headers);
// event → dispatchTriggerEventDoing the parts it doesn't support
- Keeping credentials on your own infrastructureComposio holds them by design (selfHostedCredentials: false). Route the providers you can't hand to a vendor back to integration-native via compositeProvider.
- Proxying a call yourself / provider-direct webhooksproxy: false and directWebhooks: false — Composio executes the tool and forwards a routed webhook. Verify Composio's signature at the HTTP layer, then let handleInboundWebhook parse the envelope.
- Per-agent credential isolationThe entity string is the identity, so pass the exported composioEntityId (folds tenant + user + orgScope + callerSlug). Note a per-agent entity only resolves at execute time — startConnect and trigger registration are tenant-level.
- Registering triggerssubscribeTrigger is exposed only when the injected client provides enableTrigger; without it, Composio Cloud's polling triggers aren't wired.
Related
- connectors — The IntegrationProvider seam, materializeIntegration, and compositeProvider.
- integration-native — The open default backend; compose the two so sensitive apps stay self-hosted.
- knowledge-and-tools — The host surface for managing integration connections.
- runner-background — Run the durable runs Composio's routed webhook triggers enqueue.