Adding a New Integration¶
Integrations expose third-party APIs to the AI as tools, plus optional UI for end users to authenticate and configure them.
Anatomy¶
Each integration consists of:
- A definition in
services/api/src/integrations/registry/<category>.ts. - (Optional) An OAuth handler if the service uses OAuth 2.
- (Optional) Tools the AI can call (
tools/*). - (Optional) UI for connection / config (
apps/web/components/integrations/).
The registry/ is divided into categories:
ai-ml.tscommunication.tscrm-marketing-social.tsdeveloper-tools.tsfinance-ecommerce.tsproductivity.ts
Pick the one that fits, or add a new file and re-export from registry/index.ts.
1. Define the integration¶
// services/api/src/integrations/registry/productivity.ts
export const linearIntegration = {
id: 'linear',
name: 'Linear',
category: 'productivity',
description: 'Issue tracking for fast-moving teams.',
icon: '/integrations/linear.svg',
auth: {
type: 'oauth2',
authorizeUrl: 'https://linear.app/oauth/authorize',
tokenUrl: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
clientIdEnv: 'LINEAR_CLIENT_ID',
clientSecretEnv: 'LINEAR_CLIENT_SECRET',
},
tools: [
{
name: 'linear_create_issue',
description: 'Create an issue in a Linear team.',
parameters: {
type: 'object',
properties: {
team_id: { type: 'string' },
title: { type: 'string' },
body: { type: 'string' },
},
required: ['team_id', 'title'],
},
handler: async ({ team_id, title, body }, ctx) => {
const res = await ctx.fetchOAuth('linear', 'https://api.linear.app/graphql', {
method: 'POST',
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${team_id}", title: "${title}", description: "${body ?? ''}" }) { success issue { id url } } }`,
}),
headers: { 'content-type': 'application/json' },
});
return await res.json();
},
},
],
};
2. Register¶
In services/api/src/integrations/registry/index.ts:
import { linearIntegration } from './productivity.js';
export const integrations = [
// ... existing ones
linearIntegration,
];
3. OAuth setup¶
If your provider uses OAuth 2 (most do):
- Add
LINEAR_CLIENT_ID/LINEAR_CLIENT_SECRETto.env.example. - Document the redirect URI to register in the provider's console:
https://your-host/integrations/linear/callback. - The generic OAuth flow in
services/api/src/integrations/oauth.tshandles the rest as long as yourauthblock follows the schema above.
For non-OAuth integrations (API key, basic auth):
The user enters the key in the UI; it's encrypted with ENCRYPTION_KEY and stored in integration_credentials.
4. Tool semantics¶
Each tool gets a JSON-Schema parameters block — the AI sees this as the function signature. Be terse but precise; bad descriptions waste context.
handler receives the validated arguments and a ctx object with:
ctx.fetchOAuth(integrationId, url, init)— auto-attaches the user's bearer token, refreshes if expired.ctx.workspaceId,ctx.userId,ctx.projectId.ctx.audit(action, details)— record an audit-log entry.
Return JSON-serializable data. For long results, paginate or summarize before returning — context length matters.
5. UI surfaces (optional)¶
Most integrations need only the generic connect/disconnect card. For richer UX (e.g. picking a Notion database, a Linear team), add:
and reference it from apps/web/app/(workspace)/integrations/[id]/page.tsx.
6. Tests¶
- Mock
fetchOAuthwithvi.fn(). - Validate the shape of the request (URL, headers, body) and how the handler shapes the response.
7. Docs¶
Add a short page under documentation/user-guide/integrations.md (or extend it) explaining what the integration does, its scopes, and any setup steps.
Naming convention for tools¶
<integration_id>_<verb>_<object> — e.g. linear_create_issue, gmail_send_message, stripe_list_customers. The _ separator keeps them parseable. The AI will discover them via the integration manifest at chat time; only enabled tools are exposed.
Tool-permission policy¶
Integrations honour the workspace's tool policy:
auto— runs immediately.ask— surfaces a confirmation prompt in chat.block— never runs.
By default, write actions (create_*, delete_*, send_*) ship as ask; read actions ship as auto. Set this in your tool definition with defaultPolicy: 'ask' if you have a specific recommendation.