SimStudio AI Integration Pattern: Adding New Service Integrations to the Sim Monorepo
Adding a new service integration to simstudioai/sim requires implementing a Tool in apps/sim/tools/, defining a Block in apps/sim/blocks/, registering both in their respective registry files, and wiring the UI components in the landing app catalog.
The simstudioai/sim repository orchestrates workflows through a strict, layered architecture that separates API logic from UI representation. The integration pattern ensures type-safe communication between external services and the visual workflow editor by following a consistent four-step implementation path. This guide walks through the exact file structure, configuration requirements, and code patterns used to onboard new services, using the existing Zoom integration as a canonical reference.
Step 1: Implement the Tool Layer
Tools are the low-level API clients that handle authentication, request validation, and external service communication. Each tool resides in its own directory under apps/sim/tools/<service>/.
Tool Configuration Structure
Create apps/sim/tools/<service>/index.ts and export a ToolConfig object that defines the request/response schema and OAuth requirements:
// apps/sim/tools/zoom/index.ts
import { ToolConfig } from '@/tools/types';
import { requestJson } from '@/lib/api/client/request';
import { zoomCreateMeetingContract } from '@/lib/api/contracts/zoom';
import { createLogger } from '@sim/logger';
const logger = createLogger('zoom-tool');
export const zoomCreateMeeting: ToolConfig<
{ topic: string; startTime: string },
{ joinUrl: string }
> = {
id: 'zoom_create_meeting',
name: 'Create Zoom Meeting',
description: 'Creates a new Zoom meeting and returns the join URL.',
version: '1.0.0',
oauth: { required: true, provider: 'zoom' },
request: async (params, context) => {
logger.info('Creating Zoom meeting', { topic: params.topic });
const data = await requestJson(zoomCreateMeetingContract, {
body: params,
headers: { Authorization: `Bearer ${context.accessToken}` },
});
return data;
},
};
OAuth and Request Handling
The oauth field declares whether the tool requires authentication and specifies the provider key. When oauth.required is true, the UI automatically surfaces a "Connect" button in the workflow editor. All HTTP requests must use the shared requestJson helper from @/lib/api/client/request, which validates requests against contracts defined in the apps/sim/lib/api/contracts/ directory.
Step 2: Register the Tool in the Barrel
Export the tool from the central barrel file to make it discoverable by the executor and block definitions:
// apps/sim/tools/index.ts
export { zoomCreateMeeting } from './zoom';
export { googleCalendarCreateEvent } from './google_calendar';
// Additional tool exports...
The executor scans this registry at build time to populate the available tool inventory.
Step 3: Create the Block Definition
Blocks are the visual nodes that users place on workflows. They delegate execution to Tools and define the UI inputs/outputs. Create apps/sim/blocks/blocks/<service>.ts:
// apps/sim/blocks/blocks/zoom.ts
import { BlockConfig } from '@/blocks/types';
import { zoomCreateMeeting } from '@/tools/zoom';
import { ZoomIcon } from '@/components/emcn/icons/zoom';
export const ZoomBlock: BlockConfig = {
type: 'zoom',
name: 'Zoom',
description: 'Create meetings, fetch recordings, etc.',
category: 'tools',
bgColor: '#0F9D58',
icon: ZoomIcon,
tools: {
access: ['zoom_create_meeting'],
config: {
tool: (params) => `zoom_${params.operation}`,
params: (params) => ({
topic: params.topic,
startTime: params.startTime,
}),
},
},
inputs: {
operation: { type: 'select', options: ['create_meeting'] },
topic: { type: 'short-input', required: true },
startTime: { type: 'datetime-input', required: true },
},
outputs: {
joinUrl: { type: 'short-output' },
},
};
Linking Tools to Blocks
The tools.access array lists which tool IDs the block can invoke. The tools.config.tool function maps block parameters to tool IDs, while tools.config.params transforms resolved variables into the exact shape required by the tool's request function.
Critical Implementation Rules
When configuring the tools object, follow these repository conventions to avoid runtime errors:
- Never perform type coercion in
tools.config.tool– this runs before variable resolution and cannot access runtime values. - Perform all transformations in
tools.config.params– this executes after variables are resolved and has access to the fully hydrated parameter object. - Generate resource IDs using
generateId()orgenerateShortId()from@sim/utils/idwhen creating resources in external services. - Keep Zod schemas out of block files – place all validation logic in API contracts under
apps/sim/lib/api/contracts/.
Step 4: Register the Block
Export the block from the registry to activate it in the workflow editor:
// apps/sim/blocks/registry.ts
export { ZoomBlock } from './blocks/zoom';
export { GoogleCalendarBlock } from './blocks/google_calendar';
// Additional block exports...
Step 5: Define API Contracts
All HTTP boundaries live in apps/sim/lib/api/contracts/<service>.ts. Contracts provide Zod schemas for request/response validation that are shared between the client and server:
// apps/sim/lib/api/contracts/zoom.ts
import { defineRouteContract } from '@/lib/api/contracts';
import { z } from 'zod';
export const createZoomMeetingBodySchema = z.object({
topic: z.string().min(1),
startTime: z.string().datetime(),
});
export const createZoomMeetingResponseSchema = z.object({
joinUrl: z.string().url(),
});
export const zoomCreateMeetingContract = defineRouteContract({
method: 'POST',
path: '/api/tools/zoom/createMeeting',
body: createZoomMeetingBodySchema,
response: { mode: 'json', schema: createZoomMeetingResponseSchema },
});
This pattern guarantees end-to-end type safety without duplicating validation logic.
Step 6: Configure the UI Catalog
The Integrations landing page discovers available services through a JSON registry and renders cards using dedicated components.
Add an entry to the catalog data file:
// apps/sim/app/(landing)/integrations/data/integrations.json
{
"id": "zoom",
"name": "Zoom",
"icon": "zoom",
"description": "Schedule and manage Zoom meetings directly from Sim.",
"category": "Video Conferencing"
}
Create the icon component at apps/sim/components/emcn/icons/zoom.tsx and ensure it matches the icon value in the JSON entry. The integration-card.tsx component in apps/sim/app/(landing)/integrations/components/ automatically renders the card using this data.
Step 7: Implement Optional Triggers
For services that support webhooks or event streaming, create a trigger implementation in apps/sim/triggers/<service>/:
// apps/sim/triggers/zoom/webhook.ts
import { generateId } from '@sim/utils/id';
export const handleZoomWebhook = async (payload: unknown) => {
const eventId = generateId();
// Process webhook payload and enqueue workflow execution
};
Register the trigger in apps/sim/triggers/registry.ts using the same barrel export pattern used for tools and blocks.
Step 8: Testing and Validation
Every integration must include comprehensive test coverage and pass repository-specific CI checks.
Place unit tests adjacent to the implementation files:
// apps/sim/tools/zoom/index.test.ts
import { describe, it, expect } from 'vitest';
import { zoomCreateMeeting } from './index';
describe('zoomCreateMeeting', () => {
it('should validate required parameters', async () => {
// Test implementation using global mocks from @sim/testing
});
});
Before committing, run the validation scripts to ensure compliance with repository boundaries:
bun run check:api-validation # Verifies no route-local Zod usage
bun run lint # ESLint and Prettier checks
bun test # Vitest suite execution
The check:api-validation script enforces that all Zod validation occurs within contract files, not in route handlers or tool implementations, maintaining the architectural boundary between API layers.
Summary
- Implement Tools in
apps/sim/tools/<service>/index.tsusing theToolConfigtype, handling OAuth via theoauthfield and API calls throughrequestJson. - Register Tools by exporting them from
apps/sim/tools/index.tsto make them available to the executor and block definitions. - Define Blocks in
apps/sim/blocks/blocks/<service>.tsusingBlockConfig, linking to tools via thetools.accessarray and mapping inputs viatools.config.params. - Create Contracts in
apps/sim/lib/api/contracts/<service>.tsusingdefineRouteContractto share Zod schemas between client and server. - Wire the UI by adding entries to
apps/sim/app/(landing)/integrations/data/integrations.jsonand creating icon components inapps/sim/components/emcn/icons/. - Respect Boundaries by keeping Zod schemas in contracts, using
generateId()for resource creation, and runningcheck:api-validationbefore submitting pull requests.
Frequently Asked Questions
What is the difference between a Tool and a Block in Sim?
A Tool is the low-level API client that handles HTTP requests, OAuth tokens, and external service communication, located in apps/sim/tools/. A Block is the visual workflow node that users interact with in the editor, located in apps/sim/blocks/, which delegates execution to Tools and defines the UI inputs and outputs. Blocks provide the user interface; Tools provide the implementation.
Where should Zod validation schemas be defined?
All Zod schemas must be defined in apps/sim/lib/api/contracts/<service>.ts using the defineRouteContract helper. The repository enforces this via the check:api-validation script, which fails if Zod schemas are defined directly in route handlers, tool implementations, or block configurations. This centralization ensures type safety across the client-server boundary.
How does OAuth configuration work in the Sim integration pattern?
OAuth is declared in the Tool's ToolConfig via the oauth field, specifying required: true and the provider identifier. The UI layer automatically detects this configuration and renders the appropriate "Connect" button in the integration catalog. During execution, the Tool receives the access token through the context parameter, which the Tool uses to authenticate requests to the external service.
What testing requirements exist for new integrations?
Every new Tool and Block requires unit tests placed in a *.test.ts file adjacent to the implementation. Tests must use the global mocks available from @sim/testing and verify both the Zod validation logic and the runtime behavior of the Tool. Additionally, all integrations must pass bun run check:api-validation, bun run lint, and the full bun test suite before merging.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →