d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
873 lines
26 KiB
Markdown
873 lines
26 KiB
Markdown
# 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<string, BlockConfig> = {
|
||
// ... existing blocks ...
|
||
service: ServiceBlock,
|
||
}
|
||
|
||
export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
|
||
// ... 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} <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.
|
||
|
||
## 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
|