add-integration
Skill1 starsChanged 2 months agoApache-2.0View on GitHub
Add a complete Sim integration from API docs, covering tools, block, icon, optional triggers, registrations, resolved-secret/model-input safety, and integration conventions. Use when introducing a new service under `apps/sim/tools`, `apps/sim/blocks`, and `apps/sim/triggers`.
- Sends data out
We split this skill into the rules it gives an agent
56 rules. 0 are common, 28 are distinctive, and 28 are very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
28
Distinctive
This author's own choices. Worth a look.
28
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Distinctive: line 14
Research: - Read the service's API documentation
- Distinctive: line 17
Add Icon: - Add the service's brand icon
- Distinctive: line 25
Before writing any code:
- Very specific: line 26
Use Context7 to find official documentation: mcp__context7__resolve-library-id, then fetch with mcp__context7__query-docs
- Distinctive: line 36
If the official docs do not clearly show the response JSON shape for an endpoint, you MUST stop and tell the user exactly which outputs are …
- Distinctive: line 38
Do NOT guess response field names
- Distinctive: line 39
Do NOT infer nested JSON paths from related endpoints
- Distinctive: line 40
Do NOT invent output properties just because they seem likely
- Very specific: line 41
Do NOT implement transformResponse against unverified payload shapes
- Distinctive: line 43
If response schemas are missing or incomplete, do one of the following before proceeding:
- Distinctive: line 44
Ask the user for sample responses
- Distinctive: line 45
Ask the user for test credentials so you can verify the live payload
- Distinctive: line 47
Leave the tool unimplemented and explicitly report why
- Very specific: line 118
Always use ?? null for nullable API response fields
- Very specific: line 119
Always use ?? [] for optional array fields
- Distinctive: line 121
Never output raw JSON dumps - extract meaningful fields
- Very specific: line 122
When using type: 'json' and you know the object shape, define properties with the inner fields so downstream consumers know the structure.
- Distinctive: line 123
If you do not know the response JSON shape from docs or verified examples, you MUST tell the user and stop.
- Very specific: line 134
Ordinary provider/API input: leave it unchanged. Explicit {{...}} references resolve and are sent with their normal request semantics.
- Distinctive: line 164
Never substitute secret plaintext into source or serialize plaintext provenance.
- Very specific: line 165
Never hand-roll private provenance headers/envelopes.
- Very specific: line 167
Never attach private provenance to an external URL or to directExecution.
- Distinctive: line 170
Never sanitize arbitrary third-party tool results.
- Very specific: line 172
Do not add provenance merely because a value is persisted, returned by a tool, or appears in a filename.
- Distinctive: line 180
Add focused tests covering named projection, ordinary identical text without provenance, nested and serialized shape handling, unchanged …
- Very specific: line 301
Only use canonicalParamId to link basic/advanced alternatives for the same logical parameter.
- Very specific: line 307
Every subblock id must be unique within the block.
- Very specific: line 308
Required consistency: If one subblock in a canonical group has required: true, ALL subblocks in that group must have required: true …
- Very specific: line 309
Inputs section: Must list canonical param IDs (e.g., fileId), NOT raw subblock IDs (e.g., fileSelector, manualFileId)
- Distinctive: line 310
Params function: Must use canonical param IDs, NOT raw subblock IDs (raw IDs are deleted after canonical transformation)
- Distinctive: line 367
Create a React component that spreads props
- Distinctive: line 368
Ensure viewBox is preserved from the original SVG
- Very specific: line 391
Verify with bun run check:bare-icons (also runs in CI).
- Very specific: line 397
If the service supports webhooks, create triggers using the generic buildTriggerSubBlocks helper.
- Distinctive: line 532
Do this for every visible OAuth integration.
- Very specific: line 538
Ensure the block has exactly one distinct OAuth serviceId and that it matches the canonical service entry in apps/sim/lib/oauth/oauth.ts.
- Very specific: line 549
If the canonical OAuth service has serviceAccountProviderId, run bun run deployment-config:generate to refresh …
- Distinctive: line 557
Never add a permissive fallback for missing capability metadata.
- Distinctive: line 562
Run the documentation generator:
- Distinctive: line 580
If creating V2 versions (API-aligned outputs):
- Very specific: line 582
V2 Tools: - Add _v2 suffix, version 2.0.0, flat outputs
- Very specific: line 583
V2 Block: - Add _v2 type, use createVersionedToolSelector
- Very specific: line 584
V1 Block: - Add (Legacy) to name, set hideFromToolbar: true
- Distinctive: line 677
When the user asks to add an integration:
- Very specific: line 703
When your integration handles file uploads or downloads, follow these patterns to work with UserFile objects consistently.
- Distinctive: line 728
Use the basic/advanced mode pattern:
- Very specific: line 959
Use wandConfig for fields that are hard to fill out manually:
- Very specific: line 960
Timestamps: Use generationType: 'timestamp' to inject current date context into the AI prompt
- Very specific: line 961
JSON arrays: Use generationType: 'json-object' for structured data
- Distinctive: line 962
Complex queries: Use a descriptive prompt explaining the expected format
- Distinctive: line 1002
Alphabetical ordering: - Keep imports and registry entries alphabetically sorted
- Very specific: line 1003
Required can be conditional: - Use required: { field: 'op', value: 'create' } instead of always true
- Distinctive: line 1004
DependsOn clears options: - When a dependency changes, selector options are refetched
- Very specific: line 1006
Always handle legacy file params: - Keep hidden fileContent params for backwards compatibility
- Very specific: line 1009
Never hardcode scopes: - Use getScopesForService() in blocks and getCanonicalScopesForProvider() in auth.ts
- Very specific: line 1011
OAuth service IDs need deployment capabilities: - Every visible OAuth integration must resolve through OAUTH_CLIENT_CAPABILITIES.
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Add Integration Skill
You are an expert at adding complete integrations to Sim. This skill orchestrates the full process of adding a new service integration.
Overview
Adding an integration involves these steps in order:
- Research - Read the service's API documentation
- Create Tools - Build tool configurations for each API operation
- Create Block - Build the block UI configuration
- Add Icon - Add the service's brand icon
- Create Triggers (optional) - If the service supports webhooks
- Register - Register tools, block, and triggers in their registries
- Configure Deployment Availability - Wire OAuth client and service-account metadata
- Generate and Validate the Catalog - Regenerate docs/catalog artifacts and run drift checks
Step 1: Research the API
Before writing any code:
- Use Context7 to find official documentation:
mcp__context7__resolve-library-id, then fetch withmcp__context7__query-docs - Or use WebFetch to read API docs directly
- Identify:
- Authentication method (OAuth, API Key, both)
- Available operations (CRUD, search, etc.)
- Required vs optional parameters
- Response structures
Hard Rule: No Guessed Response Schemas
If the official docs do not clearly show the response JSON shape for an endpoint, you MUST stop and tell the user exactly which outputs are unknown.
- Do NOT guess response field names
- Do NOT infer nested JSON paths from related endpoints
- Do NOT invent output properties just because they seem likely
- Do NOT implement
transformResponseagainst unverified payload shapes
If response schemas are missing or incomplete, do one of the following before proceeding:
- Ask the user for sample responses
- Ask the user for test credentials so you can verify the live payload
- Reduce the scope to only endpoints whose response shapes are documented
- Leave the tool unimplemented and explicitly report why
Step 2: Create Tools
Directory Structure
apps/sim/tools/{service}/
├── index.ts # Barrel exports
├── types.ts # TypeScript interfaces
├── {action1}.ts # Tool for action 1
├── {action2}.ts # Tool for action 2
└── ...Key Patterns
types.ts:
import type { ToolResponse } from '@/tools/types'
export interface {Service}{Action}Params {
accessToken: string // For OAuth services
// OR
apiKey: string // For API key services
requiredParam: string
optionalParam?: string
}
export interface {Service}Response extends ToolResponse {
output: {
// Define output structure
}
}Tool file pattern:
export const {service}{Action}Tool: ToolConfig<Params, Response> = {
id: '{service}_{action}',
name: '{Service} {Action}',
description: '...',
version: '1.0.0',
oauth: { required: true, provider: '{service}' }, // If OAuth
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden', description: '...' },
// ... other params
},
request: { url, method, headers, body },
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
field: data.field ?? null, // Always handle nullables
},
}
},
outputs: { /* ... */ },
}Critical Rules
visibility: 'hidden'for OAuth tokensvisibility: 'user-only'for API keys and user credentialsvisibility: 'user-or-llm'for operation parameters- Always use
?? nullfor nullable API response fields - Always use
?? []for optional array fields - Set
optional: truefor outputs that may not exist - Never output raw JSON dumps - extract meaningful fields
- When using
type: 'json'and you know the object shape, definepropertieswith the inner fields so downstream consumers know the structure. Only use baretype: 'json'when the shape is truly dynamic - If you do not know the response JSON shape from docs or verified examples, you MUST tell the user and stop. Never guess outputs or response mappings.
Resolved Secrets at Model and Persistence Boundaries
Classify every request field before implementing the tool:
This is opt-in, not a blanket integration migration. Add a model-input declaration only when the service's official documentation or an unambiguous local execution path proves that the exact field is consumed by an AI model. If that cannot be established, preserve existing tool behavior and leave the field unannotated.
- Ordinary provider/API input: leave it unchanged. Explicit
{{...}}references resolve and are sent with their normal request semantics. A URL, domain, resource ID, control field, or opaque payload is not model-visible merely because the provider is AI-backed or may process the referenced resource later. - Text or structured content consumed by an AI model: declare
request.modelInputwithmode: 'project'and select only the exact model-visible fields. The shared executor replaces activated Sim secrets with canonical{{NAME}}labels before request formatting. For nested or JSON-string fields, use a small shared selector plusapplyProjected; verify that selecting the rebuilt params reproduces the projected selection. - Serialized model content sent directly to an external provider: include the serialized
top-level param in
request.modelInput. Project the private copy before the existing request formatter parses it; keep formatter behavior deterministic when a whole-value placeholder is not valid in the serialized grammar. Do not introduce a second hard-rejection path. - Opaque model input owned by an authenticated internal route such as inline audio, image,
video, or document bytes: add
privateProvenanceto a projected request, or usemode: 'private-provenance'when there is no textual projection. Do not select storage keys, paths, signed URLs, or ordinary remote URLs as byte provenance; the owning route must authorize stored bytes independently at model egress. The route must callvalidateOpaqueModelInputProvenancebefore downloading or sending content to the model and must apply the workspace-file provenance guard before reading a persisted workspace file. - Sim-owned durable storage or internal execution handoff that can later enter a workflow/model
(table cells, Agent memory, knowledge documents/chunks, workspace-file contents, or child-workflow
input): transport encrypted field-scoped provenance with
request.secretProvenance. The authenticated receiver validates the exact selection and scope, strips the private envelope, and persists, imports, or propagates it at the owning boundary. Preserve shared legacy behavior for headerless internal calls and rows/files whose provenance marker isNULL; never invent a tool-local migration rule.
Hard rules:
- Never substitute secret plaintext into source or serialize plaintext provenance.
- Never hand-roll private provenance headers/envelopes; the shared
executeToolboundary owns transport and strips private metadata from functional results. - Never attach private provenance to an external URL or to
directExecution. Project proven model-visible external fields withrequest.modelInput; otherwise preserve ordinary request semantics. Use an authenticated internal route when encrypted provenance must cross the boundary. - Never sanitize arbitrary third-party tool results. Projection applies only to secrets activated by Sim's resolved-secret provenance for that execution/tool call.
- Do not add provenance merely because a value is persisted, returned by a tool, or appears in a
filename. Require a concrete Sim
{{...}}resolution path and a later model/log boundary. If an unsupported field can resolve a secret but does not justify durable tracking (for example afile_writepath), reject it at that exact ingress. - At diagnostic boundaries, project only values carrying execution-scoped provenance. Ordinary provider responses, filenames, URLs, and errors remain unchanged when Sim did not resolve a secret into them.
Add focused tests covering named projection, ordinary identical text without provenance, nested and
serialized shape handling, unchanged ordinary external inputs, malformed/incomplete private metadata
failing closed, headerless legacy requests, and absence of private metadata in the public tool result.
For durable sinks, also cover legacy NULL markers, exact-empty new writes, tracked secret writes,
stale/missing sidecars, and scope isolation.
Step 3: Create Block
File Location
apps/sim/blocks/blocks/{service}.ts
Block Structure
import { {Service}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const {Service}Block: BlockConfig = {
type: '{service}',
name: '{Service}',
description: '...',
longDescription: '...',
docsLink: 'https://docs.sim.ai/integrations/{service}',
category: 'tools',
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
bgColor: '#HEXCOLOR',
icon: {Service}Icon,
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
subBlocks: [
// Operation dropdown
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Operation 1', id: 'action1' },
{ label: 'Operation 2', id: 'action2' },
],
value: () => 'action1',
},
// Credential field
{
id: 'credential',
title: '{Service} Account',
type: 'oauth-input',
serviceId: '{service}',
requiredScopes: getScopesForService('{service}'),
required: true,
},
// Conditional fields per operation
// ...
],
tools: {
access: ['{service}_action1', '{service}_action2'],
config: {
tool: (params) => `{service}_${params.operation}`,
},
},
outputs: { /* ... */ },
}Key SubBlock Patterns
Condition-based visibility:
{
id: 'resourceId',
title: 'Resource ID',
type: 'short-input',
condition: { field: 'operation', value: ['read', 'update', 'delete'] },
required: { field: 'operation', value: ['read', 'update', 'delete'] },
}DependsOn for cascading selectors:
{
id: 'project',
type: 'project-selector',
dependsOn: ['credential'],
},
{
id: 'issue',
type: 'file-selector',
dependsOn: ['credential', 'project'],
}Basic/Advanced mode for dual UX:
// Basic: Visual selector
{
id: 'channelSelector',
type: 'channel-selector',
mode: 'basic',
canonicalParamId: 'channel',
dependsOn: ['credential'],
},
// Advanced: Manual input
{
id: 'channelId',
type: 'short-input',
mode: 'advanced',
canonicalParamId: 'channel',
}Note neither subblock id is channel — the canonical id is a third name that both members map
onto, and it is the only one that survives serialization.
Critical Canonical Param Rules:
canonicalParamIdmust NOT match any subblock'sidin the blockcanonicalParamIdmust be unique block-wide, not per operation.buildCanonicalIndexkeys groups bycanonicalParamIdacross all subblocks and a group holds exactly onebasicId, so two operations that each need their own pair must use two different canonical ids- Only use
canonicalParamIdto link basic/advanced alternatives for the same logical parameter. A pair carries ONE concept — for files that means upload (basic) + file reference (advanced), as in Gmail attachments (blocks/blocks/gmail.ts). Never overload the advanced side with alternate identifiers like a URL or a provider asset ID; give those their own subblocks, mark all the mutually exclusive sourcesrequired: false, and enforce "exactly one" at execution modeonly controls UI visibility, NOT serialization. WithoutcanonicalParamId, both basic and advanced field values would be sent- Every subblock
idmust be unique within the block. Duplicate IDs cause conflicts even with different conditions - Required consistency: If one subblock in a canonical group has
required: true, ALL subblocks in that group must haverequired: true(prevents bypassing validation by switching modes) - Inputs section: Must list canonical param IDs (e.g.,
fileId), NOT raw subblock IDs (e.g.,fileSelector,manualFileId) - Params function: Must use canonical param IDs, NOT raw subblock IDs (raw IDs are deleted after canonical transformation)
BlockMeta (Required)
Export a {Service}BlockMeta in the same file as the block — minimum 7 templates. See .agents/skills/add-block/SKILL.md → "BlockMeta (Required)" for valid modules and category values and the full pattern.
export const {Service}BlockMeta = {
tags: ['tag1', 'tag2'],
templates: [
{
icon: {Service}Icon,
title: '{Service} <use-case>',
prompt: 'Build a workflow that...', // concrete trigger → transformation → output
modules: ['agent', 'workflows'],
category: 'operations',
tags: ['automation'],
alsoIntegrations: ['slack'], // when the prompt references another service
},
// ... at least 6 more
],
} as const satisfies BlockMetaStep 4: Add Icon
File Location
apps/sim/components/icons.tsx
Pattern
export function {Service}Icon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* SVG paths from user-provided SVG */}
</svg>
)
}Getting Icons
Do NOT search for icons yourself. At the end of implementation, ask the user to provide the SVG:
I've completed the integration. Before I can add the icon, please provide the SVG for {Service}.
You can usually find this in the service's brand/press kit page, or copy it from their website.
Paste the SVG code here and I'll convert it to a React component.Once the user provides the SVG:
- Extract the SVG paths/content
- Create a React component that spreads props
- Ensure viewBox is preserved from the original SVG
Theme-safety (bare rendering) — REQUIRED
The icon renders both inside its colored bgColor tile AND "bare" (no tile) on a
neutral page — e.g. the home Suggested actions list — in both light and dark
mode. A monochrome logo whose paths hardcode a single near-white or near-black
fill is invisible bare on the matching background (white-on-white in light mode,
black-on-black in dark mode).
Rules when adding the SVG:
- Monochrome logos (a single white or black mark): draw the shape with
fill='currentColor', notfill='#fff'/fill='#000000'. It then inherits white inside dark tiles, near-black inside light tiles (viagetTileIconColorClass), and the theme-awarevar(--text-icon)bare — legible everywhere. Do NOT seticonColorfor these. - Multi-color brand logos (their own vivid fills): keep the hardcoded fills.
They read on any background. Only set
iconColor(a vivid brand hex, never a near-black/near-white tile color) if the bare icon should adopt a brand tint. - A large white shape with a tiny vivid accent (e.g. a logo where the body is the
white negative space) still vanishes bare — convert the body to
currentColor.
Verify with bun run check:bare-icons (also runs in CI). It flags purely
monochrome hazards; for partial-accent logos, eyeball the suggested-actions list
in both light and dark mode.
Step 5: Create Triggers (Optional)
If the service supports webhooks, create triggers using the generic buildTriggerSubBlocks helper.
Directory Structure
apps/sim/triggers/{service}/
├── index.ts # Barrel exports
├── utils.ts # Trigger options, setup instructions, extra fields
├── {event_a}.ts # Primary trigger (includes dropdown)
├── {event_b}.ts # Secondary triggers (no dropdown)
└── webhook.ts # Generic webhook (optional)Key Pattern
import { buildTriggerSubBlocks } from '@/triggers'
import { {service}TriggerOptions, {service}SetupInstructions, build{Service}ExtraFields } from './utils'
// Primary trigger - includeDropdown: true
export const {service}EventATrigger: TriggerConfig = {
id: '{service}_event_a',
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_a',
triggerOptions: {service}TriggerOptions,
includeDropdown: true, // Only for primary trigger!
setupInstructions: {service}SetupInstructions('Event A'),
extraFields: build{Service}ExtraFields('{service}_event_a'),
}),
// ...
}
// Secondary triggers - no dropdown
export const {service}EventBTrigger: TriggerConfig = {
id: '{service}_event_b',
subBlocks: buildTriggerSubBlocks({
triggerId: '{service}_event_b',
triggerOptions: {service}TriggerOptions,
// No includeDropdown!
setupInstructions: {service}SetupInstructions('Event B'),
extraFields: build{Service}ExtraFields('{service}_event_b'),
}),
// ...
}Connect to Block
import { getTrigger } from '@/triggers'
export const {Service}Block: BlockConfig = {
triggers: {
enabled: true,
available: ['{service}_event_a', '{service}_event_b'],
},
subBlocks: [
// Tool fields...
...getTrigger('{service}_event_a').subBlocks,
...getTrigger('{service}_event_b').subBlocks,
],
}See /add-trigger skill for complete documentation.
Step 6: Register Everything
Tools Registry (apps/sim/tools/registry.ts)
// Add import (alphabetically)
import {
{service}Action1Tool,
{service}Action2Tool,
} from '@/tools/{service}'
// Add to tools object (alphabetically)
export const tools: Record<string, ToolConfig> = {
// ... existing tools ...
{service}_action1: {service}Action1Tool,
{service}_action2: {service}Action2Tool,
}Then regenerate the generated tool metadata and commit it:
bun run tool-metadata:generateClient code reads params/outputs from these artifacts rather than importing
the registry, so a tool you add, change or remove is invisible to the UI until they are regenerated,
and CI fails on stale ones. See .agents/skills/tool-registry-boundary/SKILL.md.
Block Registry (apps/sim/blocks/registry-maps.ts)
The data maps (BLOCK_REGISTRY + BLOCK_META_REGISTRY) live in registry-maps.ts; registry.ts holds only the accessor functions. Add the import and an entry to each map alphabetically:
// Add import (alphabetically)
import { {Service}Block, {Service}BlockMeta } from '@/blocks/blocks/{service}'
// Add to the config map (alphabetically)
export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
// ... existing blocks ...
{service}: {Service}Block,
}
// Add to the catalog-meta map (alphabetically)
export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
// ... existing metas ...
{service}: {Service}BlockMeta,
}Trigger Registry (apps/sim/triggers/registry.ts) - If triggers exist
// Add import (alphabetically)
import {
{service}EventATrigger,
{service}EventBTrigger,
{service}WebhookTrigger,
} from '@/triggers/{service}'
// Add to TRIGGER_REGISTRY (alphabetically)
export const TRIGGER_REGISTRY: TriggerRegistry = {
// ... existing triggers ...
{service}_event_a: {service}EventATrigger,
{service}_event_b: {service}EventBTrigger,
{service}_webhook: {service}WebhookTrigger,
}Step 7: Configure Deployment Availability
Do this for every visible OAuth integration. API-key and unauthenticated integrations do not need an OAuth client capability.
The block's oauth-input.serviceId is the canonical link between the generated integration catalog,
the OAuth service configuration, deployment availability, and the setup CLI.
- Ensure the block has exactly one distinct OAuth
serviceIdand that it matches the canonical service entry inapps/sim/lib/oauth/oauth.ts. - Confirm
resolveOAuthClientCapabilityId(serviceId)resolves to the intended provider entry inOAUTH_CLIENT_CAPABILITIESinpackages/deployment-config/src/env-capabilities.ts. Google and Microsoft service IDs deliberately share provider-level capabilities. - For a new OAuth provider, add the required client fields to
OAUTH_CLIENT_CAPABILITIES, add every referenced field to the env schema inapps/sim/lib/core/config/env.ts, and add the matchingtextorsecretentries toOAUTH_CLIENT_SETUP_FIELDSinpackages/sim-setup/src/capability-config.ts. Do not create integration-specific setup logic or infer secret fields from naming; the CLI mapping is exhaustively checked against the runtime fields. - If the canonical OAuth service has
serviceAccountProviderId, runbun run deployment-config:generateto refreshpackages/deployment-config/src/service-account-providers.generated.ts; never hand-edit the generated provider-ID map. Inpackages/deployment-config/src/service-account-metadata.ts, use:- no
deploymentRequirementwhen the service-account path works independently of OAuth client fields; 'oauth-client'when it requires the same deployment OAuth client fields;'preview-gated'when availability is controlled by the service-account preview block.
- no
Never add a permissive fallback for missing capability metadata. A visible OAuth integration without a resolvable capability must fail validation.
Step 8: Generate and Validate the Catalog
Run the documentation generator:
bun run scripts/generate-docs.ts
bun run deployment-config:generate
bun run integration-catalog:check
bun run deployment-config:check
bun run docs:checkThis creates apps/docs/content/docs/en/integrations/{service}.mdx — one page per service carrying the block's Actions and, if it has one, its Triggers section. Never hand-edit generated pages; the only editable region is the {/* MANUAL-CONTENT */} block (see scripts/README.md).
The docs generator refreshes packages/deployment-config/src/integrations.json, and the deployment
config generator projects service-account provider IDs from that catalog plus the canonical OAuth
registry. The checks compare both committed projections with their sources. Review the generated
diff and keep only intentional changes.
V2 Integration Pattern
If creating V2 versions (API-aligned outputs):
- V2 Tools - Add
_v2suffix, version2.0.0, flat outputs - V2 Block - Add
_v2type, usecreateVersionedToolSelector - V1 Block - Add
(Legacy)to name, sethideFromToolbar: true - Registry - Register both versions
// In registry
{service}: {Service}Block, // V1 (legacy, hidden)
{service}_v2: {Service}V2Block, // V2 (visible)Complete Checklist
Tools
- Created
tools/{service}/directory - Created
types.tswith all interfaces - Created tool file for each operation
- All params have correct visibility
- All nullable fields use
?? null - All optional outputs have
optional: true - Created
index.tsbarrel export - Registered all tools in
tools/registry.ts - Ran
bun run tool-metadata:generateand committed the regenerated artifacts - Classified every model-visible, opaque, Sim-durable, and internal-execution request field
- Added shared model-input projection or private provenance only where required; ordinary external resource locators and control inputs retain their request semantics
- Confirmed ordinary third-party tool results are not generically sanitized
- Added provenance compatibility and fail-closed boundary tests where applicable
Block
- Created
blocks/blocks/{service}.ts - Set
integrationTypeto the correctIntegrationTypeenum value - Set
tagsarray with all applicableIntegrationTagvalues - Defined operation dropdown with all operations
- Added credential field with
requiredScopes: getScopesForService('{service}') - Added conditional fields per operation
- Set up dependsOn for cascading selectors
- Configured tools.access with all tool IDs
- Configured tools.config.tool selector
- Defined outputs matching tool outputs
- Registered block + meta in
blocks/registry-maps.ts(BLOCK_REGISTRY/BLOCK_META_REGISTRY) - If triggers: set
triggers.enabledandtriggers.available - If triggers: spread trigger subBlocks with
getTrigger() - Exported
{Service}BlockMetawith at least 7 templates
OAuth Scopes (if OAuth service)
- Defined scopes in
lib/oauth/oauth.tsunderOAUTH_PROVIDERS - Added scope descriptions in
SCOPE_DESCRIPTIONSwithinlib/oauth/utils.ts - Used
getCanonicalScopesForProvider()inauth.ts(never hardcode) - Used
getScopesForService()in blockrequiredScopes(never hardcode)
Deployment Availability (if OAuth service)
- Block declares exactly one distinct
oauth-input.serviceId resolveOAuthClientCapabilityId(serviceId)resolves to the intendedOAUTH_CLIENT_CAPABILITIESentry- Every new OAuth capability field exists in
apps/sim/lib/core/config/env.ts - Runtime OAuth fields live in
OAUTH_CLIENT_CAPABILITIES; matching CLI input modes live in the exhaustively checkedOAUTH_CLIENT_SETUP_FIELDS - If
serviceAccountProviderIdis configured,SERVICE_ACCOUNT_METADATA_BY_OAUTH_SERVICE_IDhas the matching projection and deployment requirement
Icon
- Asked user to provide SVG
- Added icon to
components/icons.tsx - Icon spreads props correctly
- Monochrome marks use
fill='currentColor'(not hardcoded white/black) so the icon renders bare in light AND dark mode — verified withbun run check:bare-icons
Triggers (if service supports webhooks)
- Created
triggers/{service}/directory - Created
utils.tswith options, instructions, and extra fields helpers - Primary trigger uses
includeDropdown: true - Secondary triggers do NOT have
includeDropdown - All triggers use
buildTriggerSubBlockshelper - Created
index.tsbarrel export - Registered all triggers in
triggers/registry.ts
Docs and deployment metadata
- Ran
bun run scripts/generate-docs.ts - Ran
bun run deployment-config:generatefor OAuth or service-account changes - Verified docs file created
- Reviewed and committed the generated
packages/deployment-config/src/integrations.jsonchange bun run integration-catalog:checkpassesbun run docs:checkpasses — CI fails on stale generated docs, so commit the full generator output, including catch-up regeneration for pages another PR left stale (never revert it as "unrelated drift")bun run deployment-config:checkpasses
Final Validation (Required)
- Read every tool file and cross-referenced inputs/outputs against the API docs
- Verified block subBlocks cover all required tool params with correct conditions
- Verified block outputs match what the tools actually return
- Verified
tools.config.paramscorrectly maps and coerces all param types - Verified every tool output and
transformResponsepath against documented or live-verified JSON responses - If any response schema remained unknown, explicitly told the user instead of guessing
{Service}BlockMetaexported with at least 7 templates, each havingicon,title,prompt,modules,category, andtags
Example Command
When the user asks to add an integration:
User: Add a Stripe integration
You: I'll add the Stripe integration. Let me:
1. First, research the Stripe API using Context7
2. Create the tools for key operations (payments, subscriptions, etc.)
3. Create the block with operation dropdown
4. Register everything
5. Generate docs
6. Ask you for the Stripe icon SVG
[Proceed with implementation...]
[After completing steps 1-5...]
I've completed the Stripe integration. Before I can add the icon, please provide the SVG for Stripe.
You can usually find this in the service's brand/press kit page, or copy it from their website.
Paste the SVG code here and I'll convert it to a React component.File Handling
When your integration handles file uploads or downloads, follow these patterns to work with UserFile objects consistently.
What is a UserFile?
A UserFile is the standard file representation in Sim:
interface UserFile {
id: string // Unique identifier
name: string // Original filename
url: string // Presigned URL for download
size: number // File size in bytes
type: string // MIME type (e.g., 'application/pdf')
base64?: string // Optional base64 content (if small file)
key?: string // Internal storage key
context?: object // Storage context metadata
}File Input Pattern (Uploads)
For tools that accept file uploads, always route through an internal API endpoint rather than calling external APIs directly. This ensures proper file content retrieval.
1. Block SubBlocks for File Input
Use the basic/advanced mode pattern:
// Basic mode: File upload UI
{
id: 'uploadFile',
title: 'File',
type: 'file-upload',
canonicalParamId: 'file', // Maps to 'file' param
placeholder: 'Upload file',
mode: 'basic',
multiple: false,
required: true,
condition: { field: 'operation', value: 'upload' },
},
// Advanced mode: Reference from previous block
{
id: 'fileRef',
title: 'File',
type: 'short-input',
canonicalParamId: 'file', // Same canonical param
placeholder: 'Reference file (e.g., {{file_block.output}})',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'upload' },
},Critical: canonicalParamId must NOT match any subblock id.
2. Normalize File Input in Block Config
In tools.config.tool, use normalizeFileInput to handle all input variants:
import { normalizeFileInput } from '@/blocks/utils'
tools: {
config: {
tool: (params) => {
// Normalize file from basic (uploadFile), advanced (fileRef), or legacy (fileContent)
const normalizedFile = normalizeFileInput(
params.uploadFile || params.fileRef || params.fileContent,
{ single: true }
)
if (normalizedFile) {
params.file = normalizedFile
}
return `{service}_${params.operation}`
},
},
}3. Create Special Internal Tool Execution Route
Create apps/sim/app/api/tools/{service}/{action}/route.ts. This raw route pattern is only for an integration's provider-execution boundary when it needs special file normalization, large-body handling, or protocol behavior. It is not the pattern for CRUD or other operations on protected Sim resources. For those, use the migrate-application-operation skill and an authorized application use case with the ordinary internal/v2 route builders.
Internal tool routes are HTTP boundaries and follow the same contract policy as public routes — define the request/response shape in apps/sim/lib/api/contracts/tools/{service}.ts (or an existing aggregate) and validate with canonical helpers from @/lib/api/server. Never write a route-local Zod schema. Authenticate and perform cheap admission before parsing or downloading files.
// apps/sim/lib/api/contracts/tools/{service}.ts
import { z } from 'zod'
import { defineRouteContract } from '@/lib/api/contracts'
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
export const {service}UploadBodySchema = z.object({
accessToken: z.string(),
file: FileInputSchema.optional().nullable(),
fileContent: z.string().optional().nullable(),
// ... other params
})
export const {service}UploadResponseSchema = z.object({
success: z.boolean(),
output: z.object({ id: z.string(), url: z.string() }).optional(),
error: z.string().optional(),
})
export const {service}UploadContract = defineRouteContract({
method: 'POST',
path: '/api/tools/{service}/upload',
body: {service}UploadBodySchema,
response: { mode: 'json', schema: {service}UploadResponseSchema },
})
export type {Service}UploadBody = z.input<typeof {service}UploadBodySchema>
export type {Service}UploadResponse = z.output<typeof {service}UploadResponseSchema>// apps/sim/app/api/tools/{service}/upload/route.ts
import { createLogger } from '@sim/logger'
import { NextResponse, type NextRequest } from 'next/server'
import { {service}UploadContract } from '@/lib/api/contracts/tools/{service}'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { type RawFileInput } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
const logger = createLogger('{Service}UploadAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
// Auth always runs BEFORE parseRequest — never validate untrusted input before authenticating.
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest({service}UploadContract, request, {})
if (!parsed.success) return parsed.response
const data = parsed.data.body
let fileBuffer: Buffer
let fileName: string
// Prefer UserFile input, fall back to legacy base64
if (data.file) {
const userFiles = processFilesToUserFiles([data.file as RawFileInput], requestId, logger)
if (userFiles.length === 0) {
return NextResponse.json({ success: false, error: 'Invalid file' }, { status: 400 })
}
const userFile = userFiles[0]
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
fileName = userFile.name
} else if (data.fileContent) {
// Legacy: base64 string (backwards compatibility)
fileBuffer = Buffer.from(data.fileContent, 'base64')
fileName = 'file'
} else {
return NextResponse.json({ success: false, error: 'File required' }, { status: 400 })
}
// Now call external API with fileBuffer
const response = await fetch('https://api.{service}.com/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${data.accessToken}` },
body: new Uint8Array(fileBuffer), // Convert Buffer for fetch
})
// ... handle response
})4. Update Tool to Use Internal Route
export const {service}UploadTool: ToolConfig<Params, Response> = {
id: '{service}_upload',
// ...
params: {
file: { type: 'file', required: false, visibility: 'user-or-llm' },
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
},
request: {
url: '/api/tools/{service}/upload', // Internal route
method: 'POST',
body: (params) => ({
accessToken: params.accessToken,
file: params.file,
fileContent: params.fileContent,
}),
},
}File Output Pattern (Downloads)
For tools that return files, use FileToolProcessor to store files and return UserFile objects.
In Tool transformResponse
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
transformResponse: async (response, context) => {
const data = await response.json()
// Process file outputs to UserFile objects
const fileProcessor = new FileToolProcessor(context)
const file = await fileProcessor.processFileData({
data: data.content, // base64 or buffer
mimeType: data.mimeType,
filename: data.filename,
})
return {
success: true,
output: { file },
}
}In API Route (for complex file handling)
// Return file data that FileToolProcessor can handle
return NextResponse.json({
success: true,
output: {
file: {
data: base64Content,
mimeType: 'application/pdf',
filename: 'document.pdf',
},
},
})Key Helpers Reference
| Helper | Location | Purpose |
normalizeFileInput | @/blocks/utils | Normalize file params in block config |
processFilesToUserFiles | @/lib/uploads/utils/file-utils | Convert raw inputs to UserFile[] |
downloadFileFromStorage | @/lib/uploads/utils/file-utils.server | Get file Buffer from UserFile |
FileToolProcessor | @/executor/utils/file-tool-processor | Process tool output files |
isUserFile | @/lib/core/utils/user-file | Type guard for UserFile objects |
FileInputSchema | @/lib/uploads/utils/file-schemas | Zod schema for file validation |
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. Examples: pagination tokens, time range filters, sort order, max results, reply settings.
WandConfig for Complex Inputs
Use wandConfig for fields that are hard to fill out manually:
- Timestamps: Use
generationType: 'timestamp'to inject current date context into the AI prompt - JSON arrays: Use
generationType: 'json-object'for structured data - Complex queries: Use a descriptive prompt explaining the expected format
{
id: 'startTime',
title: 'Start Time',
type: 'short-input',
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
generationType: 'timestamp',
},
}OAuth Scopes (Centralized System)
Scopes are maintained in a single source of truth and reused everywhere:
- Define scopes in
lib/oauth/oauth.tsunderOAUTH_PROVIDERS[provider].services[service].scopes - Add descriptions in
SCOPE_DESCRIPTIONSwithinlib/oauth/utils.tsfor the OAuth modal UI - Reference in auth.ts using
getCanonicalScopesForProvider(providerId)from@/lib/oauth/utils - Reference in blocks using
getScopesForService(serviceId)from@/lib/oauth/utils
Never hardcode scope arrays in auth.ts or block requiredScopes. Always import from the centralized source.
// In auth.ts (Better Auth config)
scopes: getCanonicalScopesForProvider('{service}'),
// In block credential sub-block
requiredScopes: getScopesForService('{service}'),Common Gotchas
- OAuth serviceId must match - The
serviceIdin oauth-input must match the OAuth provider configuration - All tool IDs MUST be snake_case -
stripe_create_payment, notstripeCreatePayment. This applies to toolidfields, registry keys,tools.accessarrays, andtools.config.toolreturn values - Block type is snake_case -
type: 'stripe', nottype: 'Stripe' - Alphabetical ordering - Keep imports and registry entries alphabetically sorted
- Required can be conditional - Use
required: { field: 'op', value: 'create' }instead of always true - DependsOn clears options - When a dependency changes, selector options are refetched
- Never pass Buffer directly to fetch - Convert to
new Uint8Array(buffer)for TypeScript compatibility - Always handle legacy file params - Keep hidden
fileContentparams for backwards compatibility - Optional fields use advanced mode - Set
mode: 'advanced'on rarely-used optional fields - Complex inputs need wandConfig - Timestamps, JSON arrays, and other hard-to-type values should have
wandConfigenabled - Never hardcode scopes - Use
getScopesForService()in blocks andgetCanonicalScopesForProvider()in auth.ts - Always add scope descriptions - New scopes must have entries in
SCOPE_DESCRIPTIONSwithinlib/oauth/utils.ts - OAuth service IDs need deployment capabilities - Every visible OAuth integration must resolve through
OAUTH_CLIENT_CAPABILITIES; shared Google/Microsoft aliases map to their provider capability - Keep runtime and presentation separate - Runtime OAuth fields live in
packages/deployment-config/src/env-capabilities.ts; CLI input modes live in the exhaustively checkedpackages/sim-setup/src/capability-config.tsmapping
Other versions7 projects use a version of this.
Word for word · 0
None found.
Edited · 1
- simstudioai/sim★ 30k36% same
Wrote their own version · 6
- kdlbs/kandev★ 1ksame name
- indranilbanerjee/digital-marketing-pro★ 920same name
- VinkyDev/vibestart★ 278same name
- incluud/astro-agent-skills★ 10same name
- calven-ai/marketing-as-code★ 9same name
- teachskillofskills-ai/DigitalMarketingPro-techshu★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository54 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .cursor/rules/constitution.mdcCursor rule
- .cursor/rules/emcn-components.mdcCursor rule
- .cursor/rules/global.mdcCursor rule
- .cursor/rules/landing-seo-geo.mdcCursor rule
- .cursor/rules/sim-architecture.mdcCursor rule
- .cursor/rules/sim-components.mdcCursor rule
- .cursor/rules/sim-hooks.mdcCursor rule
- .cursor/rules/sim-imports.mdcCursor rule
- .cursor/rules/sim-integrations.mdcCursor rule
- .cursor/rules/sim-queries.mdcCursor rule
- .cursor/rules/sim-sandbox.mdcCursor rule
- .cursor/rules/sim-stores.mdcCursor rule
- .cursor/rules/sim-styling.mdcCursor rule
- .cursor/rules/sim-testing.mdcCursor rule
- .cursor/rules/sim-typescript.mdcCursor rule
- .cursor/rules/sim-ui-copy.mdcCursor rule
- add-block-previewSkill
- add-blockSkill
- add-column-typeSkill
- add-connectorSkill
- add-enrichmentSkill
- add-feature-flagSkill
- add-hosted-keySkill
- add-managed-cliSkill
- add-modelSkill
- add-toolsSkill
- add-triggerSkill
- babysitSkill
- cleanupSkill
- councilSkill
- db-migrateSkill
- design-taste-frontendSkill
- emcn-design-reviewSkill
- emil-design-engSkill
- make-interfaces-feel-betterSkill
- memory-load-checkSkill
- migrate-application-operationSkill
- react-query-best-practicesSkill
- shipSkill
- tool-registry-boundarySkill
- v2-api-conventionsSkill
- validate-connectorSkill
- validate-integrationSkill
- validate-modelSkill
- validate-triggerSkill
- you-might-not-need-a-callbackSkill
- you-might-not-need-a-commentSkill
- you-might-not-need-a-memoSkill
- you-might-not-need-an-effectSkill
- you-might-not-need-stateSkill
- you-might-not-need-url-stateSkill
- add-settings-pageSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

