Configuring Horizon
coollabsio/coolify
A skill your agent uses whenever the user mentions Horizon by name in a Laravel context.
Create or update a Sim integration block with correct subBlocks, conditions, dependsOn, modes, canonicalParamId usage, outputs, and tool wiring.
$ npx skills add simstudioai/sim --skill add-block -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install simstudioai/sim add-block --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/simstudioai/sim.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/add-block .claude/skills/add-block && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "add-block" agent skill from https://github.com/simstudioai/sim/tree/main/.agents/skills/add-block into .claude/skills/add-block/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-block", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/simstudioai/sim/tree/main/.agents/skills/add-blockType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add simstudioai/sim --skill add-block -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install simstudioai/sim add-block --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/simstudioai/sim.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/add-block .agents/skills/add-block && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "add-block" agent skill from https://github.com/simstudioai/sim/tree/main/.agents/skills/add-block into .agents/skills/add-block/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-block", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add simstudioai/sim --skill add-block -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install simstudioai/sim add-block --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/simstudioai/sim.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/add-block .cursor/skills/add-block && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "add-block" agent skill from https://github.com/simstudioai/sim/tree/main/.agents/skills/add-block into .cursor/skills/add-block/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-block", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/simstudioai/sim.git --path .agents/skills/add-block--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add simstudioai/sim --skill add-block -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install simstudioai/sim add-block --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/simstudioai/sim.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/add-block .gemini/skills/add-block && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "add-block" agent skill from https://github.com/simstudioai/sim/tree/main/.agents/skills/add-block into .gemini/skills/add-block/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-block", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install simstudioai/sim add-blockInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add simstudioai/sim --skill add-block -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/simstudioai/sim.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/add-block .github/skills/add-block && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "add-block" agent skill from https://github.com/simstudioai/sim/tree/main/.agents/skills/add-block into .github/skills/add-block/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-block", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add simstudioai/sim --skill add-block -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install simstudioai/sim add-block --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/simstudioai/sim.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/add-block .opencode/skills/add-block && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "add-block" agent skill from https://github.com/simstudioai/sim/tree/main/.agents/skills/add-block into .opencode/skills/add-block/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-block", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
add-blockCreate or update a Sim integration block with correct subBlocks, conditions, dependsOn, modes, canonicalParamId usage, outputs, and tool wiring.
Add Block is an agent skill from simstudioai/sim. 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.
Its SKILL.md is about 10k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files (for example `agents/openai.yaml`).
It sits in Backend & APIs. The repository describes itself as: Sim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000+ builders. The licence is Apache-2.0.
3 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit b1b084d. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
bunFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
docs.sim.aiFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Add Block loads about 10k tokens when it runs. Until then it costs about 62 tokens; SKILL.md has 2,966 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from simstudioai/sim at commit b1b084d, republished under its Apache-2.0 licence (© simstudioai). 2,966 words, ~10,334 tokens.
.claude/skills/add-block/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.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}.tsBlock outputs mirror tool outputs. When a tool's response schema is neither documented nor live-verified, don't infer field names or JSON shapes — ask the user for sample responses or test credentials, limit the block to operations whose outputs are documented, or leave the uncertain outputs out and say exactly what remains unknown.
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)
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: {
// Required: the params the block accepts, keyed by tool param / canonical id
},
outputs: {
// Define outputs available to downstream blocks
},
}Critical: Give every subblock a unique id: duplicates collide silently (the last definition wins). blocks.test.ts fails a duplicate within one condition unless the copies are a basic/advanced mode-swap pair, one basic plus trigger-mode copies, or all carry canonicalParamId. The only sanctioned cross-condition reuse is the hosted-key apiKey pair (add-hosted-key skill), where both fields deliberately share one value.
// 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',
canonicalParamId: 'oauthCredential',
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.
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:
serviceId across the block's oauth-input subBlocks.OAUTH_CLIENT_CAPABILITIES. Google and
Microsoft service IDs intentionally share their provider-level capability; do not add duplicate
entries for those aliases.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.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.
// Channel selector (Slack, Discord, etc.)
{
id: 'channel',
title: 'Channel',
type: 'channel-selector',
selectorKey: '{service}.channels',
serviceId: '{service}',
placeholder: 'Select channel',
dependsOn: ['credential'],
}
// Project selector (Jira, etc.)
{
id: 'project',
title: 'Project',
type: 'project-selector',
selectorKey: '{service}.projects',
serviceId: '{service}',
dependsOn: ['credential'],
}
// File selector (Google Drive, etc.)
{
id: 'file',
title: 'File',
type: 'file-selector',
selectorKey: '{service}.files',
serviceId: '{service}',
mimeType: 'application/pdf',
dependsOn: ['credential'],
}
// User selector
{
id: 'user',
title: 'User',
type: 'user-selector',
selectorKey: '{service}.users',
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.
Constraints (block-wide):
canonicalParamId may equal only the id of a member of its own group, as channel does in the canonicalParamId Pattern below; it must never equal any other subblock's id. (blocks.test.ts enforces the case of a subblock with no canonicalParamId.)basicId, so two operations that each need a pair need two canonical ids.required status.Put 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)' },
}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 configuration'trigger-advanced' - The advanced side of a trigger field (a canonical pair member, or a standalone field under the block-level advanced toggle)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:
channel selector value → params.channelchannelId input value → params.channelEnables 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 contextUse wandConfig on fields that are hard to fill by hand — timestamps (generationType: 'timestamp' injects the current date), comma-separated ID lists, complex query strings. Keep the prompt specific about the return format (e.g. 'Return ONLY the ISO 8601 timestamp string').
Write operation ids and tool ids as string literals in the operation dropdown options and
tools.access, never through constants (id: SEARCH, access: [SEARCH]). scripts/generate-docs.ts
reads them from source with regexes, so a constant parses as empty: the docs page loses its whole
Actions section and the integration catalog ships blank operation descriptions, with no check failing.
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', 'file', 'file[]', 'any')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' },
}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. 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)',
},
}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
// Required: drives the amber legacy badge and its click-to-upgrade action.
// `check-block-registry` fails a legacy block with no `replacedBy`, one whose
// target does not exist, or one whose target is itself sunset or still `preview`.
sunset: { status: 'legacy', replacedBy: 'service_v2' },
// ... 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?.tool(params) ?? 'service_default',
suffix: '_v2',
fallbackToolId: 'service_default_v2',
}),
params: ServiceBlock.tools?.config?.params,
},
},
outputs: {
// Flat, API-aligned outputs (not wrapped in content/metadata)
},
}Register the block in apps/sim/blocks/registry-maps.ts — 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,
bgColor: '#FF6B6B',
icon: ServiceIcon,
authMode: AuthMode.OAuth,
// Sentence rules: apps/sim/blocks/AGENTS.md → "Canvas sentences"
canvasPresentation: {
defaultTitle: 'Create Resource',
sentences: {
byOperation: {
create: [{ text: 'Create resource', field: 'name', core: true }],
read: [{ text: 'Read resource', field: 'resourceId', core: true }],
update: [{ text: 'Update resource', field: 'resourceId', core: true }],
delete: [{ text: 'Delete resource', field: 'resourceId', core: true }],
},
},
},
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',
canonicalParamId: 'oauthCredential',
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}`,
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
oauthCredential: { type: 'string', description: 'Service access token' },
resourceId: { type: 'string', description: 'Resource ID' },
name: { type: 'string', description: 'Resource name' },
},
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
}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.Every block declares a one-line prose summary that replaces its card's field rows:
Slack ← header (already names the block)
Post ⟨Ship it 🚀⟩ to ⟨#eng⟩ ← the sentence; ⟨…⟩ are live value chipsWrite 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 four mistakes that break
cards silently — is apps/sim/blocks/AGENTS.md → "Canvas sentences". Read it
before writing any. Two of those four are worth repeating here, because both
are invisible at runtime:
canonicalParamId pair drops the sentence
for every advanced-mode user. List all members:
field: ['channelSelector', 'manualChannel'].condition excludes that operation can
never render.Validate before finishing:
bun run apps/sim/scripts/check-canvas-sentences.ts --block={service}When adding or changing sunset.replacedBy, run bun run generate:block-successors and commit
apps/sim/lib/permission-groups/block-successors.generated.ts. Authorization uses this generated
map to resolve legacy and current block IDs consistently without importing the executable registry.
Verify it with bun run check:block-successors.
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 reads those from the generated metadata, not 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:
bun run tool-metadata:generate (only when a tool changed), bun run scripts/generate-docs.ts,
bun run deployment-config:generate, then bun run check:audits. Also run
bun run apps/sim/scripts/check-block-registry.ts origin/staging (CI runs it outside check:audits). Commit the
full generator output. For what each check verifies, see the validate-integration skill →
Regenerate Derived Artifacts.
integrationType is set to the correct IntegrationType enum value{Service}BlockMeta.tags lists every applicable IntegrationTag (tags live on the meta, not the block)id, title (except switch), and typeserviceId and requiredScopes: getScopesForService(serviceId)serviceId resolves through resolveOAuthClientCapabilityId() to the correct OAUTH_CLIENT_CAPABILITIES entryapps/sim/lib/core/config/env.tsSERVICE_ACCOUNT_METADATA_BY_OAUTH_SERVICE_ID matches its canonical serviceAccountProviderId and deployment requirementSCOPE_DESCRIPTIONS in lib/oauth/utils.ts for any new scopesBLOCK_REGISTRY / BLOCK_META_REGISTRY)sunset.replacedBy changed: regenerated and committed the block successor map; bun run check:block-successors passesbun run tool-metadata:generate and committed the artifactsbun run scripts/generate-docs.ts, reviewed the generated diff, and committed the integration catalog changesbun run integration-catalog:check passesbun run docs:check passes (CI gate — fails on any stale generated docs page)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)canvasPresentation.sentences covers every operation, and bun run apps/sim/scripts/check-canvas-sentences.ts --block={service} passes with 100% coverageValidate the block against every tool in tools.access:
tools.accesscondition to show for that operation)canonicalParamId) is named exactly after the tool param it fills. A required user-only param that is only renamed in tools.config.params fails bun run apps/sim/scripts/check-block-registry.ts origin/staging; remap only optional or user-or-llm paramstools.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 tagsInternalToolConfig.operation or an absolute external
HTTP(S) ToolConfig.request. If transport needs to change, use the add-tools skill; never add a
same-origin /api/... hop or a directExecution property from the block.selectorKey or options, never a per-block fetcherA sub-block gets its choices from exactly one of two places. There is no third.
selectorKey — every remote list. Use the add-selector skill to add browser-safe metadata in
apps/sim/lib/selectors/manifest.ts. Attach provider-server selectors under
apps/sim/lib/selectors/server/providers/ and internal-server selectors in
apps/sim/lib/selectors/server/internal.ts. Point the sub-block at that key. All remote selectors
execute through selectors.execute; never add a client provider module or selector-only fetch route.
{ 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. The
shared context builder projects only active dependsOn values and keys canonical pairs by their
canonical id. Exact environment references such as {{GMAIL_CREDENTIAL_ID}} stay unresolved in the
browser and are resolved only by the authorized server executor. The builder does not infer a
nonstandard credential id from type: 'oauth-input'; give it
canonicalParamId: 'oauthCredential', or declare an explicit manifest sourceFields alias when a
legacy source id must be retained.
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.
Two rules the checks enforce:
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.© simstudioai, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 1 other file in .agents/skills/add-block of simstudioai/sim.
Open the folder on GitHubat commit b1b084d
Add Block next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Add Block this skillsimstudioai/sim | 30k | — | ~10k | Automated safety check: Pass | Apache-2.0 | |
| Configuring Horizoncoollabsio/coolify | 63k | 4 repos | ~898 | Automated safety check: Pass | MIT | |
| Nestjs Best Practicesrolling-scopes/rsschool-app | 10k | 6 repos | ~1.2k | Automated safety check: Pass | MIT | |
| Sub2API AdminWei-Shaw/sub2api | 44k | 1 repos | ~717 | Automated safety check: Pass | LGPL-3.0 | |
| Firecrawl Build Onboardingfirecrawl/firecrawl | 190k | 1 repos | ~1.4k | Automated safety check: Notes | ISC | |
| Obsidian BasesAtmosphere/atmosphere | 3.8k | 22 repos | ~3.2k | Automated safety check: Pass | Apache-2.0 |
coollabsio/coolify
A skill your agent uses whenever the user mentions Horizon by name in a Laravel context.
rolling-scopes/rsschool-app
NestJS best practices and architecture patterns for building production-ready applications.
Wei-Shaw/sub2api
Manages a Sub2API deployment from the command line: accounts, redeem and invitation codes, groups, proxies, imports, exports and raw admin API calls.
firecrawl/firecrawl
Gets Firecrawl working in a project: signs you in through the browser, saves FIRECRAWL_API_KEY to .env and picks the first SDK or REST path.
Atmosphere/atmosphere
Create and edit Obsidian Bases (.base files) with views, filters, formulas, and summaries.
coollabsio/coolify
ACTIVATE when the user works on authentication in Laravel. An agent skill from coollabsio/coolify.
simstudioai/sim
Install, upgrade, and operate the Sim Helm chart on Kubernetes.
simstudioai/sim
Add a new table column type to Sim — registry entry, icon, storage shape, coercion, and the behavioral hooks the grid and API read.
simstudioai/sim
Add a code-defined table enrichment (registry entry) under apps/sim/enrichments/ backed by an ordered provider cascade, ensuring every provider tool it calls has hosted-key support.
simstudioai/sim
Add hosted API key support to a tool so Sim provides the key (metered and billed to the workspace) when a user has not brought their own.
simstudioai/sim
Add or upgrade a curated, immutable managed CLI for Sim Function sandboxes, including client-safe catalog metadata, a pinned server-only installation recipe, checksum and executable verification…
simstudioai/sim
Add or update a Sim dynamic selector using the shared manifest, server attachment, and selectors.execute path.
Categories
Create or update a Sim integration block with correct subBlocks, conditions, dependsOn, modes, canonicalParamId usage, outputs, and tool wiring. Add Block is an agent skill from simstudioai/sim. Create or update a Sim integration block with correct subBlocks, conditions, dependsOn, modes, canonicalParamId usage, outputs, and tool wiring.
Add Block fits situations like: working on apps/sim/blocks/blocks/{service}.ts; aligning a block with its tools.
Run `npx skills add simstudioai/sim --skill add-block -a claude-code`. Or copy the skill folder (.agents/skills/add-block in simstudioai/sim) into .claude/skills/add-block in your project. Claude Code loads it when a task matches its description.
Run `npx skills add simstudioai/sim --skill add-block -a codex`. Or copy the skill folder (.agents/skills/add-block in simstudioai/sim) into .agents/skills/add-block in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add simstudioai/sim --skill add-block -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/add-block, .gemini/skills/add-block, .github/skills/add-block and .opencode/skills/add-block in your project.
Going by SKILL.md and its folder, Add Block needs the command-line tools its instructions call (bun).
SKILL.md names 1 domain. In commands or code: docs.sim.ai; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Add Block is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 10k tokens (SKILL.md is roughly 41k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Add Block: Configuring Horizon (coollabsio/coolify, 63k stars), Nestjs Best Practices (rolling-scopes/rsschool-app, 10k stars), Sub2API Admin (Wei-Shaw/sub2api, 44k stars) and Firecrawl Build Onboarding (firecrawl/firecrawl, 190k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
simstudioai (a GitHub organization) maintains it in simstudioai/sim, which has 29,792 GitHub stars. The repository holds 40 skills in this directory. The repository was last updated on October 9, 2026.
Source: simstudioai/sim on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.