Agent Skillssimstudioai/sim › add-block

add-block

GitHub

用于在 Sim 平台创建或更新集成 Block,配置 subBlocks、工具关联及输出结构。适用于开发集成模块时遵循严格的数据验证与配置规范。

.agents/skills/add-block/SKILL.md simstudioai/sim

Trigger Scenarios

创建新的 Sim 集成 Block 更新现有 Block 的配置与工具连接

Install

npx skills add simstudioai/sim --skill add-block -g -y
More Options

Non-standard path

npx skills add https://github.com/simstudioai/sim/tree/main/.agents/skills/add-block -g -y

Use without installing

npx skills use simstudioai/sim@add-block

指定 Agent (Claude Code)

npx skills add simstudioai/sim --skill add-block -a claude-code -g -y

安装 repo 全部 skill

npx skills add simstudioai/sim --all -g -y

预览 repo 内 skill

npx skills add simstudioai/sim --list

SKILL.md

Frontmatter
{
    "name": "add-block",
    "description": "Create or update a Sim integration block with correct subBlocks, conditions, dependsOn, modes, canonicalParamId usage, outputs, and tool wiring. Use when working on `apps\/sim\/blocks\/blocks\/{service}.ts` or aligning a block with its tools.",
    "argument-hint": "<service-name>"
}

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

Hard Rule: No Guessed Tool Outputs

Blocks depend on tool outputs. If the underlying tool response schema is not documented or live-verified, you MUST tell the user instead of guessing block outputs.

  • Do NOT invent block outputs for undocumented tool responses
  • Do NOT describe unknown JSON shapes as if they were confirmed
  • Do NOT wire fields into the block just because they seem likely to exist

If the tool outputs are not known, do one of these instead:

  1. Ask the user for sample tool responses
  2. Ask the user for test credentials so the tool responses can be verified
  3. Limit the block to operations whose outputs are documented
  4. Leave uncertain outputs out and explicitly tell the user what remains unknown

Block Configuration Structure

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/integrations/{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

  // Card summary sentences — see "Canvas Sentences" below
  canvasPresentation: {
    defaultTitle: '{Default Operation}',
    sentences: { byOperation: { /* one per operation dropdown option id */ } },
  },

  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

// 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

// 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

{
  id: 'code',
  title: 'Code',
  type: 'code',
  language: 'javascript',  // 'javascript' | 'json' | 'python'
  placeholder: '// Enter code...',
}

OAuth/Credentials

{
  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.

Service accounts (shared, app-level credentials): A plain oauth-input already lets users select an existing service account — those credentials fold into the picker automatically (a Google service account created for any Google service appears in every Google block's picker). You only set credentialKind when you want to change the connect action:

{
  id: 'credential',
  title: 'Account',
  type: 'oauth-input',
  serviceId: '{service}',
  requiredScopes: getScopesForService('{service}'),
  credentialKind: 'any',   // omit | 'service-account' | 'any'
}
  • omit (default): lists OAuth accounts + any existing service accounts; the only connect action is "Connect account" (OAuth). Use this for the common "let users pick a service account someone set up elsewhere, but don't offer inline setup" case — no config needed.
  • 'service-account': service-account credentials only, plus an inline setup action that opens the provider's connect modal. Use when a block accepts only an app credential.
  • 'any': merged picker — OAuth accounts and service accounts in one grouped dropdown, with a connect action for each. Use when a block supports both (e.g. Slack: a personal account or a custom bot).

Optional companions: credentialLabels (override the picker's section/connect-row copy) and allowServiceAccounts: true (trigger-mode only — list service accounts, which triggers otherwise exclude; set only when the trigger's polling path can resolve a service-account token). The connect modal, provider families (Google JSON key, Atlassian token, token-paste, client-credential, Slack bot), and the preview gate are all resolved from serviceAccountProviderId — you don't wire them per block.

OAuth deployment availability (required for integration blocks)

A visible tools-category block with OAuth is deployment-gated. Its oauth-input.serviceId is projected into packages/deployment-config/src/integrations.json, then resolved through resolveOAuthClientCapabilityId() in packages/deployment-config/src/env-capabilities.ts.

When adding or changing an OAuth integration block:

  1. Keep exactly one distinct OAuth serviceId across the block's oauth-input subBlocks.
  2. Confirm that service ID resolves to an entry in OAUTH_CLIENT_CAPABILITIES. Google and Microsoft service IDs intentionally share their provider-level capability; do not add duplicate entries for those aliases.
  3. For a new capability, add its required client fields to OAUTH_CLIENT_CAPABILITIES and ensure every referenced field exists in the env schema in apps/sim/lib/core/config/env.ts. Then add the matching text or secret input modes to OAUTH_CLIENT_SETUP_FIELDS in packages/sim-setup/src/capability-config.ts. The CLI catalog is exhaustively typed and checked against the runtime field list; do not infer secrecy from the field name.
  4. If the canonical OAuth service declares serviceAccountProviderId, run bun run deployment-config:generate; this regenerates the provider-ID facts in packages/deployment-config/src/service-account-providers.generated.ts. Never hand-edit that generated map. Add deploymentRequirement policy in packages/deployment-config/src/service-account-metadata.ts only when the service-account path is preview-gated or depends on the OAuth client fields; otherwise omit it.

Missing capability metadata is a runtime configuration error, not a reason to make the integration silently available.

Selectors (with dynamic options)

// 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

// 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

// 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' },
},

Keep the pair to one logical thing. Basic is the file upload, advanced is only a reference to a file from a previous block. Gmail attachments are the reference implementation (apps/sim/blocks/blocks/gmail.tsattachmentFiles / attachments).

Do not overload the advanced side with alternate identifiers (a remote URL, a provider asset ID, a path). A subblock whose meaning changes based on what the string looks like is impossible to reason about, forces the params function to sniff the value, and makes the field's type meaningless. Give each alternative its own subblock outside the pair:

// ✓ Good — the pair is "a file"; other sources are their own fields
{ id: 'mediaFile',    type: 'file-upload', canonicalParamId: 'media', mode: 'basic' },
{ id: 'mediaFileRef', type: 'short-input', canonicalParamId: 'media', mode: 'advanced' },
{ id: 'mediaId',      type: 'short-input', mode: 'advanced' },  // separate concept
{ id: 'mediaLink',    type: 'short-input', mode: 'advanced' },  // separate concept

// ✗ Bad — one field meaning three things, resolved by guessing
{ id: 'mediaRef', type: 'short-input', canonicalParamId: 'media', mode: 'advanced',
  placeholder: 'File reference, media ID, or public URL' },

When several fields are mutually exclusive alternatives, mark them all required: false and enforce "exactly one" at execution — a conditionally-required canonical pair rejects the workflow before the other paths ever get a chance to supply the value.

Critical constraints:

  • canonicalParamId must NOT match any subblock's id in the same block
  • A canonical group is block-wide, not per-operation: buildCanonicalIndex keys groups by canonicalParamId across every subblock, and a group has exactly one basicId. Two operations that each need a file pair need two distinct canonicalParamId values.
  • All members of a group must share the same required status

Normalizing File Input in tools.config

Put the normalization in tools.config.params, never in tools.config.tooltool runs at serialization, before variable resolution, so a <block.output> file reference is not yet a value there.

import { normalizeFileInput } from '@/blocks/utils'

tools: {
  access: ['service_upload'],
  config: {
    tool: (params) => `service_${params.operation}`,
    params: (params) => {
      // Read the CANONICAL id, not the subblock ids
      const { file: fileParam, ...rest } = params
      const file = normalizeFileInput(fileParam, { single: true })
      return {
        ...rest,
        ...(file ? { file } : {}),
      }
    },
  },
}

Where the value actually lives at runtime. The subblock id is where the UI stores the value, but it is not what the params function receives. extractBlockParams (apps/sim/serializer/index.ts) collapses each canonical group at serialization time:

const sourceIds = [group.basicId, ...group.advancedIds].filter(Boolean)
sourceIds.forEach((id) => delete params[id])   // subblock ids are deleted
if (chosen !== undefined) params[group.canonicalId] = chosen

So by the time tools.config.params(inputs) runs (executor/handlers/generic/generic-handler.ts), params.uploadFile and params.fileRef are gone and the value is under params.file. Reading a subblock id there yields undefined and silently sends no file.

Only the active mode's value survives — getCanonicalValues returns the basic value in basic mode and the first non-empty advanced value in advanced mode, so a stale value in the dormant mode can never leak. normalizeFileInput then handles the JSON string that advanced-mode template resolution produces.

Note that generic-handler merges rather than replaces ({ ...inputs, ...transformedParams }), so omitting a key from the returned object does not strip it from what the tool receives. Tools simply ignore params they do not declare.

File Input Types in inputs

Declare the canonical id with type: 'json' — the subblock ids never reach inputs:

inputs: {
  file: { type: 'json', description: 'File to upload (UserFile or reference)' },
  // Legacy field for backwards compatibility
  fileContent: { type: 'string', description: 'Legacy: base64 encoded content' },
}

Multiple Files

For multiple file uploads:

{
  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

condition: { field: 'operation', value: 'create' }
// Shows when operation === 'create'

Multiple Values (OR)

condition: { field: 'operation', value: ['create', 'update'] }
// Shows when operation is 'create' OR 'update'

Negation

condition: { field: 'operation', value: 'delete', not: true }
// Shows when operation !== 'delete'

Compound (AND)

condition: {
  field: 'operation',
  value: 'send',
  and: {
    field: 'type',
    value: 'dm',
    not: true,
  }
}
// Shows when operation === 'send' AND type !== 'dm'

Complex Example

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)

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)

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

required: true
required: false

Conditional Required

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:

// 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)
  • A canonicalParamId links exactly one basic/advanced pair for a single logical parameter. Do NOT reuse the same canonicalParamId for different parameters, even under mutually-exclusive conditions/operations
  • 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.

{
  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:

// 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

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

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
  • condition - Optional visibility condition
  • hiddenFromDisplay - Optional flag to hide from the output display

Nested object/properties outputs are tool-output-only and will fail TypeScript at build time on block outputs. For complex shapes use type: 'json' and describe the inner fields in the description string.

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. Block outputs have no nested properties form — always keep the output flat and put the shape in the description:

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)',
  },
}

Nested object outputs (plan: { id: { type: 'string' }, ... }) are a tool-output feature only — OutputFieldDefinition for blocks does not allow them and they fail TypeScript at build time.

If the output shape is unknown because the underlying tool response is undocumented, you MUST tell the user and stop. Unknown is not the same as variable. Never guess block outputs.

V2 Block Pattern

When creating V2 blocks (alongside legacy V1):

// 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:

import { ServiceBlock, ServiceBlockMeta } from '@/blocks/blocks/service'

export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
  // ... existing blocks ...
  service: ServiceBlock,
}

export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
  // ... existing metas ...
  service: ServiceBlockMeta,
}

Complete Example

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/integrations/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.

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.

When converting the SVG: a monochrome logo (single white or black mark) must use fill='currentColor', never a hardcoded #fff/#000000. Block icons render both inside their bgColor tile and "bare" on a neutral page (the home Suggested actions list) in light and dark mode; a hardcoded white/black mark goes invisible bare on the matching background. Multi-color brand logos keep their own fills. Verify with bun run check:bare-icons.

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
{
  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.

// 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.

import type { BlockMeta } from '@/blocks/types'

export const {Service}BlockMeta = {
  tags: ['tag1', 'tag2'],                  // IntegrationTag[]
  url: 'https://{service}.com',            // external service homepage (verify it resolves) — NOT docs.sim.ai
  templates: [
    {
      icon: {Service}Icon,
      title: '{Service} <use-case>',        // 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.

Canvas Sentences

Every block declares a one-line prose summary that replaces its card's field rows:

Slack                                    ← header (already names the block)
Posts ⟨Ship it 🚀⟩ to ⟨#eng⟩             ← the sentence; ⟨…⟩ are live value chips

Write one byOperation entry per operation dropdown option (or a single default when the block has no operation dropdown).

The full authoring contract — voice, structure, and the two mistakes that break cards silently — is apps/sim/blocks/AGENTS.md → "Canvas sentences". Read it before writing any. The two failures worth repeating here, because both are invisible at runtime:

  1. A clause naming only one member of a canonicalParamId pair drops the sentence for every advanced-mode user. List all members: field: ['channelSelector', 'manualChannel'].
  2. A clause referencing a subblock whose condition excludes that operation can never render.

Validate before finishing:

bun run apps/sim/scripts/check-canvas-sentences.ts --block={service}

Generated artifacts

Adding a block on its own needs no tool metadata regeneration — a block references existing tool IDs through tools.access and does not change any tool's shape.

But if the same change also adds, edits or removes a tool, run bun run tool-metadata:generate and commit the result, or CI fails on stale artifacts. That matters here because a block's outputs are authored to match its tools' outputs, and the UI now reads those from the generated metadata rather than the executable registry — an unregenerated tool change makes the block's outputs disagree with what the panel renders. See .agents/skills/tool-registry-boundary/SKILL.md.

A visible integration block does require the generated integration catalog and docs to be refreshed. After adding or changing one, run:

bun run scripts/generate-docs.ts
bun run deployment-config:generate
bun run integration-catalog:check
bun run deployment-config:check
bun run docs:check

The catalog check independently derives deployment metadata from the executable block registry and compares it with the committed packages/deployment-config/src/integrations.json. The deployment config check verifies the generated service-account facts against the canonical OAuth registry and catalog. docs:check re-renders every generated docs artifact in memory and fails on any committed file that differs — it runs in CI via check:audits, so commit the full generator output. If the generator also trues up pages an earlier PR left stale, commit that catch-up too; reverting it as "unrelated drift" makes docs:check fail. Review the generated diff and keep only intentional changes.

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)
  • Every OAuth serviceId resolves through resolveOAuthClientCapabilityId() to the correct OAUTH_CLIENT_CAPABILITIES entry
  • Any new OAuth capability fields exist in apps/sim/lib/core/config/env.ts
  • If the OAuth service supports service accounts, SERVICE_ACCOUNT_METADATA_BY_OAUTH_SERVICE_ID matches its canonical serviceAccountProviderId and deployment requirement
  • 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 any tool was added, changed or removed alongside the block: ran bun run tool-metadata:generate and committed the artifacts
  • Ran bun run scripts/generate-docs.ts, reviewed the generated diff, and committed the integration catalog changes
  • bun run integration-catalog:check passes
  • bun run docs:check passes (CI gate — fails on any stale generated docs page)
  • 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
  • url set on {Service}BlockMeta to the external service's verified homepage (omit only for first-party blocks with no external service)
  • skills added to {Service}BlockMeta, each grounded in tools.access and sourced from a real online use case (not invented)
  • canvasPresentation.sentences covers every operation, and bun run apps/sim/scripts/check-canvas-sentences.ts --block={service} passes with 100% coverage

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
  5. Verify {Service}BlockMeta is exported with at least 7 templates, each having icon, title, prompt, modules, category, and tags
  6. If any tool outputs are still unknown, explicitly tell the user instead of guessing block outputs

Option Lists: selectorKey or options, never a per-block fetcher

A sub-block gets its choices from exactly one of two places. There is no third.

selectorKey — every remote list. Register the list in hooks/selectors/providers/<service>/selectors.ts, add its key to SelectorKey, and point the sub-block at it. A selector is parameterized by an explicit SelectorContext, so the same definition serves the canvas, the workspace-fork sync modal, and anything added later.

{ id: 'triggerCredentials', type: 'oauth-input', canonicalParamId: 'oauthCredential', mode: 'trigger' },
{ id: 'labelIds', type: 'dropdown', multiSelect: true,
  selectorKey: 'gmail.labels', dependsOn: ['triggerCredentials'], mode: 'trigger' },
{ id: 'manualLabelIds', type: 'short-input', mode: 'trigger-advanced' },

canonicalParamId: 'oauthCredential' on the credential sub-block is the line people forget. buildSelectorContextFromBlock keys the context on a sub-block's CANONICAL id, so without it context.oauthCredential is never set and the picker looks unfixable without reading the store. (A credential field is also recognised by its oauth-input TYPE as a fallback, so a block whose shipped param is already named something else does not have to rename it.)

options — everything else. A static array, or a pure function of the block's own values for a list that narrows to a sibling's selection. No I/O.

options: (params) => {
  const model = params?.values.model
  return typeof model === 'string' ? effortsFor(model) : DEFAULT_EFFORTS
}

Never fetch inside options, and never reach into the stores from a block definition. A fetcher that resolves its credential with readSubBlockValue(blockId, ...) only works on the canvas — every surface that is not the editor gets an empty list. fetchOptions/fetchOptionById were removed for exactly this reason.

Two rules the checks enforce:

  • A secret never enters a selector's getQueryKey. A query key identifies a resource; a credential authorizes access to it. A credential id is fine; a typed password is not (see imap.mailboxes).
  • A sub-block that dependsOn a credential / knowledge-base / table selector must be reconfigurable at fork-sync time — a selectorKey, a canonical pair whose basic member is a selector, or a short-input/long-input. bun run check:fork-dependent-coverage fails otherwise, because a fork sync clears those fields on every push and an unofferable one can never be set anywhere that sticks.

Version History

  • ceda457 Current 2026-08-20 15:28

Same Skill Collection

.agents/skills/add-block-preview/SKILL.md
.agents/skills/add-column-type/SKILL.md
.agents/skills/add-connector/SKILL.md
.agents/skills/add-enrichment/SKILL.md
.agents/skills/add-feature-flag/SKILL.md
.agents/skills/add-hosted-key/SKILL.md
.agents/skills/add-integration/SKILL.md
.agents/skills/add-managed-cli/SKILL.md
.agents/skills/add-model/SKILL.md
.agents/skills/add-tools/SKILL.md
.agents/skills/add-trigger/SKILL.md
.agents/skills/babysit/SKILL.md
.agents/skills/cleanup/SKILL.md
.agents/skills/council/SKILL.md
.agents/skills/db-migrate/SKILL.md
.agents/skills/design-taste-frontend/SKILL.md
.agents/skills/emcn-design-review/SKILL.md
.agents/skills/emil-design-eng/SKILL.md
.agents/skills/make-interfaces-feel-better/SKILL.md
.agents/skills/memory-load-check/SKILL.md
.agents/skills/react-query-best-practices/SKILL.md
.agents/skills/ship/SKILL.md
.agents/skills/tool-registry-boundary/SKILL.md
.agents/skills/v2-api-conventions/SKILL.md
.agents/skills/validate-connector/SKILL.md
.agents/skills/validate-integration/SKILL.md
.agents/skills/validate-model/SKILL.md
.agents/skills/validate-trigger/SKILL.md
.agents/skills/you-might-not-need-a-callback/SKILL.md
.agents/skills/you-might-not-need-a-comment/SKILL.md
.agents/skills/you-might-not-need-a-memo/SKILL.md
.agents/skills/you-might-not-need-an-effect/SKILL.md
.agents/skills/you-might-not-need-state/SKILL.md
.agents/skills/you-might-not-need-url-state/SKILL.md
.claude/skills/add-settings-page/SKILL.md
helm/sim/.claude/skills/sim-helm/SKILL.md
.agents/skills/migrate-application-operation/SKILL.md

Metadata

Files
0
Version
ceda457
Hash
c8d64377
Indexed
2026-08-20 15:28

- 위키
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-25 08:47
浙ICP备14020137号-1 $방문자$