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.
66
81%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
You are an expert at creating block configurations for Sim. You understand the serializer, subBlock types, conditions, dependsOn, modes, and all UI patterns.
When the user asks you to create a block:
apps/sim/blocks/blocks/{service}.tsBlocks 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.
If the tool outputs are not known, do one of these instead:
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
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
},
}Critical: Every subblock id must be unique within the block. Duplicate IDs cause conflicts even with different conditions.
// 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 }// 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,
}{
id: 'code',
title: 'Code',
type: 'code',
language: 'javascript', // 'javascript' | 'json' | 'python'
placeholder: '// Enter code...',
}{
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'
}'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.
// 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'],
}// 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',
}When your block accepts file uploads, use the basic/advanced mode pattern with normalizeFileInput.
// 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.ts — attachmentFiles / 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 blockbuildCanonicalIndex 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.required statusPut the normalization in tools.config.params, never in tools.config.tool — tool 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] = chosenSo 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.
inputsDeclare 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' },
}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
}Controls when a field is shown based on other field values.
condition: { field: 'operation', value: 'create' }
// Shows when operation === 'create'condition: { field: 'operation', value: ['create', 'update'] }
// Shows when operation is 'create' OR 'update'condition: { field: 'operation', value: 'delete', not: true }
// Shows when operation !== 'delete'condition: {
field: 'operation',
value: 'send',
and: {
field: 'type',
value: 'dm',
not: true,
}
}
// Shows when operation === 'send' AND type !== 'dm'condition: {
field: 'operation',
value: ['list', 'search'],
not: true,
and: {
field: 'authMethod',
value: 'oauth',
}
}
// Shows when operation NOT in ['list', 'search'] AND authMethod === 'oauth'Controls when a field is enabled and when its options are refetched.
dependsOn: ['credential']
// Enabled only when credential has a value
// Options refetch when credential changes
dependsOn: ['credential', 'projectId']
// Enabled only when BOTH have valuesdependsOn: {
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)Can be boolean or condition-based.
required: true
required: falserequired: { field: 'operation', value: 'create' }
// Required only when operation === 'create'
required: { field: 'operation', value: ['create', 'update'] }
// Required when operation is 'create' OR 'update'Controls which UI view shows the field.
'basic' - Only in basic view (default UI)'advanced' - Only in advanced view'both' - Both views (default if not specified)'trigger' - Only in trigger configurationMaps 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:
channel selector value → params.channelchannelId input value → params.channelCritical constraints:
canonicalParamId must NOT match any other subblock's id in the same block (causes conflicts)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/operationscanonicalParamId to link basic/advanced mode alternatives for the same logical parameterEnables 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
},
}'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 contextImportant: 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,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,
}),
},
}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',
}),
},
}IMPORTANT: Block outputs have a simpler schema than tool outputs. Block outputs do NOT support:
optional: true - This is only for tool outputsitems property - This is only for tool outputs with array typesBlock outputs only support:
type - The data type ('string', 'number', 'boolean', 'json', 'array')description - Human readable descriptioncondition - Optional visibility conditionhiddenFromDisplay - Optional flag to hide from the output displayNested 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' },
},
}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.
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)
},
}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,
}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' },
},
}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.
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.
Optional fields that are rarely used should be set to mode: 'advanced' so they don't clutter the basic UI. This includes:
{
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
}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.',
},
}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.
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 BlockMetaDerive 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:
tools.access. Never describe an action the integration cannot perform.integrationType is set to the correct IntegrationType enum valuetags array includes all applicable IntegrationTag valuesid, title (except switch), and typeserviceId and requiredScopes: getScopesForService(serviceId)SCOPE_DESCRIPTIONS in lib/oauth/utils.ts for any new scopesBLOCK_REGISTRY / BLOCK_META_REGISTRY)triggers config set, trigger subBlocks spreadmode: 'advanced'wandConfig enabled{Service}BlockMeta with at least 7 templatesurl 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)After creating the block, you MUST validate it against every tool it references:
tools.access — do not skip anycondition to show for that operation)tools.config.params correctly maps subBlock IDs to tool param names (if they differ)tools.config.params for any params that need conversion (Number(), Boolean(), JSON.parse()){Service}BlockMeta is exported with at least 7 templates, each having icon, title, prompt, modules, category, and tags6f514c1
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.