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() or generateShortId() from @sim/utils/id when 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.ts using the ToolConfig type, handling OAuth via the oauth field and API calls through requestJson.
  • Register Tools by exporting them from apps/sim/tools/index.ts to make them available to the executor and block definitions.
  • Define Blocks in apps/sim/blocks/blocks/<service>.ts using BlockConfig, linking to tools via the tools.access array and mapping inputs via tools.config.params.
  • Create Contracts in apps/sim/lib/api/contracts/<service>.ts using defineRouteContract to share Zod schemas between client and server.
  • Wire the UI by adding entries to apps/sim/app/(landing)/integrations/data/integrations.json and creating icon components in apps/sim/components/emcn/icons/.
  • Respect Boundaries by keeping Zod schemas in contracts, using generateId() for resource creation, and running check:api-validation before 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →