# Add Block Skill You are an expert at creating block configurations for Sim. You understand the serializer, subBlock types, conditions, dependsOn, modes, and all UI patterns. ## Your Task When the user asks you to create a block: 1. Create the block file in `apps/sim/blocks/blocks/{service}.ts` 2. Configure all subBlocks with proper types, conditions, and dependencies 3. Wire up tools correctly ## Block Configuration Structure ```typescript import { {ServiceName}Icon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' import { getScopesForService } from '@/lib/oauth/utils' export const {ServiceName}Block: BlockConfig = { type: '{service}', // snake_case identifier name: '{Service Name}', // Human readable description: 'Brief description', // One sentence longDescription: 'Detailed description for docs', docsLink: 'https://docs.sim.ai/tools/{service}', category: 'tools', // 'tools' | 'blocks' | 'triggers' integrationType: IntegrationType.X, // Primary category (see IntegrationType enum) tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type) bgColor: '#HEXCOLOR', // Brand color icon: {ServiceName}Icon, // Auth mode authMode: AuthMode.OAuth, // or AuthMode.ApiKey subBlocks: [ // Define all UI fields here ], tools: { access: ['tool_id_1', 'tool_id_2'], // Array of tool IDs this block can use config: { tool: (params) => `{service}_${params.operation}`, // Tool selector function params: (params) => ({ // Transform subBlock values to tool params }), }, }, inputs: { // Optional: define expected inputs from other blocks }, outputs: { // Define outputs available to downstream blocks }, } ``` ## SubBlock Types Reference **Critical:** Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions. ### Text Inputs ```typescript // Single-line input { id: 'field', title: 'Label', type: 'short-input', placeholder: '...' } // Multi-line input { id: 'field', title: 'Label', type: 'long-input', placeholder: '...', rows: 6 } // Password input { id: 'apiKey', title: 'API Key', type: 'short-input', password: true } ``` ### Selection Inputs ```typescript // Dropdown (static options) { id: 'operation', title: 'Operation', type: 'dropdown', options: [ { label: 'Create', id: 'create' }, { label: 'Update', id: 'update' }, ], value: () => 'create', // Default value function } // Combobox (searchable dropdown) { id: 'field', title: 'Label', type: 'combobox', options: [...], searchable: true, } ``` ### Code/JSON Inputs ```typescript { id: 'code', title: 'Code', type: 'code', language: 'javascript', // 'javascript' | 'json' | 'python' placeholder: '// Enter code...', } ``` ### OAuth/Credentials ```typescript { id: 'credential', title: 'Account', type: 'oauth-input', serviceId: '{service}', // Must match OAuth provider service key requiredScopes: getScopesForService('{service}'), // Import from @/lib/oauth/utils placeholder: 'Select account', required: true, } ``` **Scopes:** Always use `getScopesForService(serviceId)` from `@/lib/oauth/utils` for `requiredScopes`. Never hardcode scope arrays — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`. **Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`. ### Selectors (with dynamic options) ```typescript // Channel selector (Slack, Discord, etc.) { id: 'channel', title: 'Channel', type: 'channel-selector', serviceId: '{service}', placeholder: 'Select channel', dependsOn: ['credential'], } // Project selector (Jira, etc.) { id: 'project', title: 'Project', type: 'project-selector', serviceId: '{service}', dependsOn: ['credential'], } // File selector (Google Drive, etc.) { id: 'file', title: 'File', type: 'file-selector', serviceId: '{service}', mimeType: 'application/pdf', dependsOn: ['credential'], } // User selector { id: 'user', title: 'User', type: 'user-selector', serviceId: '{service}', dependsOn: ['credential'], } ``` ### Other Types ```typescript // Switch/toggle { id: 'enabled', type: 'switch' } // Slider { id: 'temperature', title: 'Temperature', type: 'slider', min: 0, max: 2, step: 0.1 } // Table (key-value pairs) { id: 'headers', title: 'Headers', type: 'table', columns: ['Key', 'Value'] } // File upload { id: 'files', title: 'Attachments', type: 'file-upload', multiple: true, acceptedTypes: 'image/*,application/pdf', } ``` ## File Input Handling When your block accepts file uploads, use the basic/advanced mode pattern with `normalizeFileInput`. ### Basic/Advanced File Pattern ```typescript // Basic mode: Visual file upload { id: 'uploadFile', title: 'File', type: 'file-upload', canonicalParamId: 'file', // Both map to 'file' param placeholder: 'Upload file', mode: 'basic', multiple: false, required: true, condition: { field: 'operation', value: 'upload' }, }, // Advanced mode: Reference from other blocks { id: 'fileRef', title: 'File', type: 'short-input', canonicalParamId: 'file', // Both map to 'file' param placeholder: 'Reference file (e.g., {{file_block.output}})', mode: 'advanced', required: true, condition: { field: 'operation', value: 'upload' }, }, ``` **Critical constraints:** - `canonicalParamId` must NOT match any subblock's `id` in the same block - Values are stored under subblock `id`, not `canonicalParamId` ### Normalizing File Input in tools.config Use `normalizeFileInput` to handle all input variants: ```typescript import { normalizeFileInput } from '@/blocks/utils' tools: { access: ['service_upload'], config: { tool: (params) => { // Check all field IDs: uploadFile (basic), fileRef (advanced), fileContent (legacy) const normalizedFile = normalizeFileInput( params.uploadFile || params.fileRef || params.fileContent, { single: true } ) if (normalizedFile) { params.file = normalizedFile } return `service_${params.operation}` }, }, } ``` **Why this pattern?** - Values come through as `params.uploadFile` or `params.fileRef` (the subblock IDs) - `canonicalParamId` only controls UI/schema mapping, not runtime values - `normalizeFileInput` handles JSON strings from advanced mode template resolution ### File Input Types in `inputs` Use `type: 'json'` for file inputs: ```typescript inputs: { uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' }, fileRef: { type: 'json', description: 'File reference from previous block' }, // Legacy field for backwards compatibility fileContent: { type: 'string', description: 'Legacy: base64 encoded content' }, } ``` ### Multiple Files For multiple file uploads: ```typescript { id: 'attachments', title: 'Attachments', type: 'file-upload', multiple: true, // Allow multiple files maxSize: 25, // Max size in MB per file acceptedTypes: 'image/*,application/pdf,.doc,.docx', } // In tools.config: const normalizedFiles = normalizeFileInput( params.attachments || params.attachmentRefs, // No { single: true } - returns array ) if (normalizedFiles) { params.files = normalizedFiles } ``` ## Condition Syntax Controls when a field is shown based on other field values. ### Simple Condition ```typescript condition: { field: 'operation', value: 'create' } // Shows when operation === 'create' ``` ### Multiple Values (OR) ```typescript condition: { field: 'operation', value: ['create', 'update'] } // Shows when operation is 'create' OR 'update' ``` ### Negation ```typescript condition: { field: 'operation', value: 'delete', not: true } // Shows when operation !== 'delete' ``` ### Compound (AND) ```typescript condition: { field: 'operation', value: 'send', and: { field: 'type', value: 'dm', not: true, } } // Shows when operation === 'send' AND type !== 'dm' ``` ### Complex Example ```typescript condition: { field: 'operation', value: ['list', 'search'], not: true, and: { field: 'authMethod', value: 'oauth', } } // Shows when operation NOT in ['list', 'search'] AND authMethod === 'oauth' ``` ## DependsOn Pattern Controls when a field is enabled and when its options are refetched. ### Simple Array (all must be set) ```typescript dependsOn: ['credential'] // Enabled only when credential has a value // Options refetch when credential changes dependsOn: ['credential', 'projectId'] // Enabled only when BOTH have values ``` ### Complex (all + any) ```typescript dependsOn: { all: ['authMethod'], // All must be set any: ['credential', 'apiKey'] // At least one must be set } // Enabled when authMethod is set AND (credential OR apiKey is set) ``` ## Required Pattern Can be boolean or condition-based. ### Simple Boolean ```typescript required: true required: false ``` ### Conditional Required ```typescript required: { field: 'operation', value: 'create' } // Required only when operation === 'create' required: { field: 'operation', value: ['create', 'update'] } // Required when operation is 'create' OR 'update' ``` ## Mode Pattern (Basic vs Advanced) Controls which UI view shows the field. ### Mode Options - `'basic'` - Only in basic view (default UI) - `'advanced'` - Only in advanced view - `'both'` - Both views (default if not specified) - `'trigger'` - Only in trigger configuration ### canonicalParamId Pattern Maps multiple UI fields to a single serialized parameter: ```typescript // Basic mode: Visual selector { id: 'channel', title: 'Channel', type: 'channel-selector', mode: 'basic', canonicalParamId: 'channel', // Both map to 'channel' param dependsOn: ['credential'], } // Advanced mode: Manual input { id: 'channelId', title: 'Channel ID', type: 'short-input', mode: 'advanced', canonicalParamId: 'channel', // Both map to 'channel' param placeholder: 'Enter channel ID manually', } ``` **How it works:** - In basic mode: `channel` selector value → `params.channel` - In advanced mode: `channelId` input value → `params.channel` - The serializer consolidates based on current mode **Critical constraints:** - `canonicalParamId` must NOT match any other subblock's `id` in the same block (causes conflicts) - `canonicalParamId` must be unique per block (only one basic/advanced pair per canonicalParamId) - ONLY use `canonicalParamId` to link basic/advanced mode alternatives for the same logical parameter - Do NOT use it for any other purpose ## WandConfig Pattern Enables AI-assisted field generation. ```typescript { id: 'query', title: 'Query', type: 'code', language: 'json', wandConfig: { enabled: true, prompt: 'Generate a query based on the user request. Return ONLY the JSON.', placeholder: 'Describe what you want to query...', generationType: 'json-object', // Optional: affects AI behavior maintainHistory: true, // Optional: keeps conversation context }, } ``` ### Generation Types - `'javascript-function-body'` - JS code generation - `'json-object'` - Raw JSON (adds "no markdown" instruction) - `'json-schema'` - JSON Schema definitions - `'sql-query'` - SQL statements - `'timestamp'` - Adds current date/time context ## Tools Configuration **Important:** `tools.config.tool` runs during serialization before variable resolution. Put `Number()` and other type coercions in `tools.config.params` instead, which runs at execution time after variables are resolved. **Preferred:** Use tool names directly as dropdown option IDs to avoid switch cases: ```typescript // Dropdown options use tool IDs directly options: [ { label: 'Create', id: 'service_create' }, { label: 'Read', id: 'service_read' }, ] // Tool selector just returns the operation value tool: (params) => params.operation, ``` ### With Parameter Transformation ```typescript tools: { access: ['service_action'], config: { tool: (params) => 'service_action', params: (params) => ({ id: params.resourceId, data: typeof params.data === 'string' ? JSON.parse(params.data) : params.data, }), }, } ``` ### V2 Versioned Tool Selector ```typescript import { createVersionedToolSelector } from '@/blocks/utils' tools: { access: [ 'service_create_v2', 'service_read_v2', 'service_update_v2', ], config: { tool: createVersionedToolSelector({ baseToolSelector: (params) => `service_${params.operation}`, suffix: '_v2', fallbackToolId: 'service_create_v2', }), }, } ``` ## Outputs Definition **IMPORTANT:** Block outputs have a simpler schema than tool outputs. Block outputs do NOT support: - `optional: true` - This is only for tool outputs - `items` property - This is only for tool outputs with array types Block outputs only support: - `type` - The data type ('string', 'number', 'boolean', 'json', 'array') - `description` - Human readable description - Nested object structure (for complex types) ```typescript outputs: { // Simple outputs id: { type: 'string', description: 'Resource ID' }, success: { type: 'boolean', description: 'Whether operation succeeded' }, // Use type: 'json' for complex objects or arrays (NOT type: 'array' with items) items: { type: 'json', description: 'List of items' }, metadata: { type: 'json', description: 'Response metadata' }, // Nested outputs (for structured data) user: { id: { type: 'string', description: 'User ID' }, name: { type: 'string', description: 'User name' }, email: { type: 'string', description: 'User email' }, }, } ``` ### Typed JSON Outputs When using `type: 'json'` and you know the object shape in advance, **describe the inner fields in the description** so downstream blocks know what properties are available. For well-known, stable objects, use nested output definitions instead: ```typescript outputs: { // BAD: Opaque json with no info about what's inside plan: { type: 'json', description: 'Zone plan information' }, // GOOD: Describe the known fields in the description plan: { type: 'json', description: 'Zone plan information (id, name, price, currency, frequency, is_subscribed)', }, // BEST: Use nested output definition when the shape is stable and well-known plan: { id: { type: 'string', description: 'Plan identifier' }, name: { type: 'string', description: 'Plan name' }, price: { type: 'number', description: 'Plan price' }, currency: { type: 'string', description: 'Price currency' }, }, } ``` Use the nested pattern when: - The object has a small, stable set of fields (< 10) - Downstream blocks will commonly access specific properties - The API response shape is well-documented and unlikely to change Use `type: 'json'` with a descriptive string when: - The object has many fields or a dynamic shape - It represents a list/array of items - The shape varies by operation ## V2 Block Pattern When creating V2 blocks (alongside legacy V1): ```typescript // V1 Block - mark as legacy export const ServiceBlock: BlockConfig = { type: 'service', name: 'Service (Legacy)', hideFromToolbar: true, // Hide from toolbar // ... rest of config } // V2 Block - visible, uses V2 tools export const ServiceV2Block: BlockConfig = { type: 'service_v2', name: 'Service', // Clean name hideFromToolbar: false, // Visible subBlocks: ServiceBlock.subBlocks, // Reuse UI tools: { access: ServiceBlock.tools?.access?.map(id => `${id}_v2`) || [], config: { tool: createVersionedToolSelector({ baseToolSelector: (params) => (ServiceBlock.tools?.config as any)?.tool(params), suffix: '_v2', fallbackToolId: 'service_default_v2', }), params: ServiceBlock.tools?.config?.params, }, }, outputs: { // Flat, API-aligned outputs (not wrapped in content/metadata) }, } ``` ## Registering Blocks After creating the block, remind the user to register it in `apps/sim/blocks/registry-maps.ts` (the data maps live here; `registry.ts` holds only the accessor functions). Add the import and an entry to each map alphabetically: ```typescript import { ServiceBlock, ServiceBlockMeta } from '@/blocks/blocks/service' export const BLOCK_REGISTRY: Record = { // ... existing blocks ... service: ServiceBlock, } export const BLOCK_META_REGISTRY: Record = { // ... existing metas ... service: ServiceBlockMeta, } ``` ## Complete Example ```typescript import { ServiceIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' import { getScopesForService } from '@/lib/oauth/utils' export const ServiceBlock: BlockConfig = { type: 'service', name: 'Service', description: 'Integrate with Service API', longDescription: 'Full description for documentation...', docsLink: 'https://docs.sim.ai/tools/service', category: 'tools', integrationType: IntegrationType.DeveloperTools, tags: ['oauth', 'api'], bgColor: '#FF6B6B', icon: ServiceIcon, authMode: AuthMode.OAuth, subBlocks: [ { id: 'operation', title: 'Operation', type: 'dropdown', options: [ { label: 'Create', id: 'create' }, { label: 'Read', id: 'read' }, { label: 'Update', id: 'update' }, { label: 'Delete', id: 'delete' }, ], value: () => 'create', }, { id: 'credential', title: 'Service Account', type: 'oauth-input', serviceId: 'service', requiredScopes: getScopesForService('service'), placeholder: 'Select account', required: true, }, { id: 'resourceId', title: 'Resource ID', type: 'short-input', placeholder: 'Enter resource ID', condition: { field: 'operation', value: ['read', 'update', 'delete'] }, required: { field: 'operation', value: ['read', 'update', 'delete'] }, }, { id: 'name', title: 'Name', type: 'short-input', placeholder: 'Resource name', condition: { field: 'operation', value: ['create', 'update'] }, required: { field: 'operation', value: 'create' }, }, ], tools: { access: ['service_create', 'service_read', 'service_update', 'service_delete'], config: { tool: (params) => `service_${params.operation}`, }, }, outputs: { id: { type: 'string', description: 'Resource ID' }, name: { type: 'string', description: 'Resource name' }, createdAt: { type: 'string', description: 'Creation timestamp' }, }, } ``` ## Connecting Blocks with Triggers If the service supports webhooks, connect the block to its triggers. ```typescript import { getTrigger } from '@/triggers' export const ServiceBlock: BlockConfig = { // ... basic config ... triggers: { enabled: true, available: ['service_event_a', 'service_event_b', 'service_webhook'], }, subBlocks: [ // Tool subBlocks first... { id: 'operation', /* ... */ }, // Then spread trigger subBlocks ...getTrigger('service_event_a').subBlocks, ...getTrigger('service_event_b').subBlocks, ...getTrigger('service_webhook').subBlocks, ], } ``` See the `/add-trigger` skill for creating triggers. ## Icon Requirement If the icon doesn't already exist in `@/components/icons.tsx`, **do NOT search for it yourself**. After completing the block, ask the user to provide the SVG: ``` The block is complete, but I need an icon for {Service}. Please provide the SVG and I'll convert it to a React component. You can usually find this in the service's brand/press kit page, or copy it from their website. ``` ## Advanced Mode for Optional Fields Optional fields that are rarely used should be set to `mode: 'advanced'` so they don't clutter the basic UI. This includes: - Pagination tokens - Time range filters (start/end time) - Sort order options - Reply settings - Rarely used IDs (e.g., reply-to tweet ID, quote tweet ID) - Max results / limits ```typescript { id: 'startTime', title: 'Start Time', type: 'short-input', placeholder: 'ISO 8601 timestamp', condition: { field: 'operation', value: ['search', 'list'] }, mode: 'advanced', // Rarely used, hide from basic view } ``` ## WandConfig for Complex Inputs Use `wandConfig` for fields that are hard to fill out manually, such as timestamps, comma-separated lists, and complex query strings. This gives users an AI-assisted input experience. ```typescript // Timestamps - use generationType: 'timestamp' to inject current date context { id: 'startTime', title: 'Start Time', type: 'short-input', mode: 'advanced', wandConfig: { enabled: true, prompt: 'Generate an ISO 8601 timestamp based on the user description. Return ONLY the timestamp string.', generationType: 'timestamp', }, } // Comma-separated lists - simple prompt without generationType { id: 'mediaIds', title: 'Media IDs', type: 'short-input', mode: 'advanced', wandConfig: { enabled: true, prompt: 'Generate a comma-separated list of media IDs. Return ONLY the comma-separated values.', }, } ``` ## Naming Convention All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MUST use `snake_case` (e.g., `x_create_tweet`, `slack_send_message`). Never use camelCase or PascalCase. ## BlockMeta (Required) Every block file must export a `{Service}BlockMeta` alongside the block — **minimum 7 templates**. Look at existing examples in `apps/sim/blocks/blocks/` (e.g. `browser_use.ts`, `google_sheets.ts`) for the pattern. ```typescript import type { BlockMeta } from '@/blocks/types' export const {Service}BlockMeta = { tags: ['tag1', 'tag2'], // IntegrationTag[] templates: [ { icon: {Service}Icon, title: '{Service} ', // 2–5 words prompt: 'Build a workflow that...', // specific use case, 1–3 sentences modules: ['agent', 'workflows'], // 'agent' | 'workflows' | 'tables' | 'files' | 'scheduled' | 'knowledge-base' category: 'operations', // 'operations' | 'marketing' | 'sales' | 'engineering' | 'productivity' | 'support' | 'popular' tags: ['automation'], alsoIntegrations: ['slack'], // optional — other block IDs referenced in the prompt featured: true, // optional }, // ... at least 6 more ], skills: [ // SuggestedSkill[] — 3–5 mainstream, 2–3 niche { name: 'summarize-thread', // kebab-case, ≤64 chars, unique, verb-led description: 'One line: what it does and when to use it.', // ≤1024 chars content: '# Summarize Thread\n\n...\n\n## Steps\n1. ...\n\n## Output\n...', // markdown }, // ... more ], } as const satisfies BlockMeta ``` Derive templates from the service's real use cases. Each prompt should name a concrete trigger, transformation, and output — not a generic description of what the service does. `skills` are curated, ready-to-add agent skills shown on the integration's detail page (users click **Add** to create them in their workspace). Two hard rules: - **Ground every skill in operations the block actually exposes** — cross-check each skill's steps against `tools.access`. Never describe an action the integration cannot perform. - **Derive skills from real, popular use cases found online — never invent them.** Web-search the service's documented use cases (vendor use-case/solutions pages, official docs describing the workflow, reputable "top automations for X" articles) and only add a skill you can source as something people genuinely do with the service. Do not hallucinate skills. ## Checklist Before Finishing - [ ] `integrationType` is set to the correct `IntegrationType` enum value - [ ] `tags` array includes all applicable `IntegrationTag` values - [ ] All subBlocks have `id`, `title` (except switch), and `type` - [ ] Conditions use correct syntax (field, value, not, and) - [ ] DependsOn set for fields that need other values - [ ] Required fields marked correctly (boolean or condition) - [ ] OAuth inputs have correct `serviceId` and `requiredScopes: getScopesForService(serviceId)` - [ ] Scope descriptions added to `SCOPE_DESCRIPTIONS` in `lib/oauth/utils.ts` for any new scopes - [ ] Tools.access lists all tool IDs (snake_case) - [ ] Tools.config.tool returns correct tool ID (snake_case) - [ ] Outputs match tool outputs - [ ] Block + meta registered in registry-maps.ts (`BLOCK_REGISTRY` / `BLOCK_META_REGISTRY`) - [ ] If icon missing: asked user to provide SVG - [ ] If triggers exist: `triggers` config set, trigger subBlocks spread - [ ] Optional/rarely-used fields set to `mode: 'advanced'` - [ ] Timestamps and complex inputs have `wandConfig` enabled - [ ] Exported `{Service}BlockMeta` with at least 7 templates - [ ] `skills` added to `{Service}BlockMeta`, each grounded in `tools.access` and sourced from a real online use case (not invented) ## Final Validation (Required) After creating the block, you MUST validate it against every tool it references: 1. **Read every tool definition** that appears in `tools.access` — do not skip any 2. **For each tool, verify the block has correct:** - SubBlock inputs that cover all required tool params (with correct `condition` to show for that operation) - SubBlock input types that match the tool param types (e.g., dropdown for enums, short-input for strings) - `tools.config.params` correctly maps subBlock IDs to tool param names (if they differ) - Type coercions in `tools.config.params` for any params that need conversion (Number(), Boolean(), JSON.parse()) 3. **Verify block outputs** cover the key fields returned by all tools 4. **Verify conditions** — each subBlock should only show for the operations that actually use it