* improvement(oauth): centralize scopes and remove dead scope evaluation code Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(oauth): fix stale scope-descriptions.ts references and add test coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
23 KiB
description, argument-hint
| description | argument-hint |
|---|---|
| Create a block configuration for a Sim integration with proper subBlocks, conditions, and tool wiring | <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:
- Create the block file in
apps/sim/blocks/blocks/{service}.ts - Configure all subBlocks with proper types, conditions, and dependencies
- Wire up tools correctly
Block Configuration Structure
import { {ServiceName}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } 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'
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
// 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.
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' },
},
Critical constraints:
canonicalParamIdmust NOT match any subblock'sidin the same block- Values are stored under subblock
id, notcanonicalParamId
Normalizing File Input in tools.config
Use normalizeFileInput to handle all input variants:
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.uploadFileorparams.fileRef(the subblock IDs) canonicalParamIdonly controls UI/schema mapping, not runtime valuesnormalizeFileInputhandles JSON strings from advanced mode template resolution
File Input Types in inputs
Use type: 'json' for file inputs:
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:
{
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:
channelselector value →params.channel - In advanced mode:
channelIdinput value →params.channel - The serializer consolidates based on current mode
Critical constraints:
canonicalParamIdmust NOT match any other subblock'sidin the same block (causes conflicts)canonicalParamIdmust be unique per block (only one basic/advanced pair per canonicalParamId)- ONLY use
canonicalParamIdto 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 outputsitemsproperty - 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)
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:
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):
// 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:
- Import in
apps/sim/blocks/registry.ts - Add to the
registryobject (alphabetically):
import { ServiceBlock } from '@/blocks/blocks/service'
export const registry: Record<string, BlockConfig> = {
// ... existing blocks ...
service: ServiceBlock,
}
Complete Example
import { ServiceIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } 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',
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.
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.
Checklist Before Finishing
- All subBlocks have
id,title(except switch), andtype - 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
serviceIdandrequiredScopes: getScopesForService(serviceId) - Scope descriptions added to
SCOPE_DESCRIPTIONSinlib/oauth/utils.tsfor 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 registered in registry.ts
- If icon missing: asked user to provide SVG
- If triggers exist:
triggersconfig set, trigger subBlocks spread - Optional/rarely-used fields set to
mode: 'advanced' - Timestamps and complex inputs have
wandConfigenabled
Final Validation (Required)
After creating the block, you MUST validate it against every tool it references:
- Read every tool definition that appears in
tools.access— do not skip any - For each tool, verify the block has correct:
- SubBlock inputs that cover all required tool params (with correct
conditionto show for that operation) - SubBlock input types that match the tool param types (e.g., dropdown for enums, short-input for strings)
tools.config.paramscorrectly maps subBlock IDs to tool param names (if they differ)- Type coercions in
tools.config.paramsfor any params that need conversion (Number(), Boolean(), JSON.parse())
- SubBlock inputs that cover all required tool params (with correct
- Verify block outputs cover the key fields returned by all tools
- Verify conditions — each subBlock should only show for the operations that actually use it