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`.
69
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
You are an expert at adding complete integrations to Sim. This skill orchestrates the full process of adding a new service integration.
Adding an integration involves these steps in order:
Before writing any code:
mcp__context7__resolve-library-id, then fetch with mcp__context7__query-docsRead the provider's current authentication documentation first. “OAuth” describes a protocol; it does not imply a browser redirect or a deployment-wide client ID and secret. Compare the supported paths and choose the simplest supported setup for the intended user:
| Provider method | Sim connection pattern | Verify in the provider docs |
|---|---|---|
| Authorization code | Shared OAuth app and consent flow | Partner approval, redirect URIs, tenant consent, scopes, refresh-token rotation |
| Customer-owned client credentials | Saved service_account credential with a client ID and secret | Internal-app eligibility, grant activation, scope syntax, token lifetime and revocation behavior |
| API token or personal access token | Saved token service account when reusable connections are useful | Token permissions, identity verification, expiry and rotation |
| Private key, certificate, or service-account JSON | Existing key-based service-account framework | Signing algorithm, audience, subject, tenant binding and key rotation |
Do not present customer-owned credentials as a way around a provider's approval requirements for a shared public integration. Explain the supported account/app type in the setup docs. Keep existing OAuth connections usable when other users depend on them, unless a migration or removal is explicitly authorized.
For client credentials, extend the existing descriptors and minter registry under
apps/sim/lib/credentials/client-credential-accounts/; for token credentials, use
token-service-accounts/. Reuse the connect modal, encrypted storage, authorized credential
operations, and execution-time token resolver. Do not collect reusable secrets on every block,
invent a credential route, or store a short-lived access token as if it were permanent.
Verify the complete lifecycle, including connect-time verification, concurrent executions on different workers, expiry, secret rotation, and permission changes. Some providers invalidate the previous token whenever another is minted. Those providers require shared coordination keyed by the provider application identity, including across duplicate saved credentials; a process-local or per-scope token cache is insufficient. A cache hit must never authenticate a wrong secret or silently grant broader permissions. Bound token responses and retries, prevent credential-bearing redirects, and never include provider response bodies or secrets in errors.
Use explicit permission choices when the supported operations have different access needs. Keep region and permission selections on reconnect unless the user changes them. Verify documented identity endpoints rather than guessing which person a client-credentials token represents.
oauth-input is the shared saved-credential picker, including for service accounts that never
redirect to OAuth. Give its basic/advanced pair one canonicalParamId: 'oauthCredential', with
the correct serviceId, and wire tool OAuth metadata to that same service. Tokens and trusted
API origins are hidden execution inputs; the model receives a credential ID, never a secret.
Use credentialKind: 'service-account' when only service accounts are supported, including in
tool OAuth metadata. Use credentialKind: 'any' on the picker when both browser OAuth and saved
service accounts are supported; omitting it defaults the connect action to browser OAuth. Per-connection scope
choices belong in the descriptor and encrypted credential, not an all-permissions block
requiredScopes array that would reject read-only connections.
When a documented list endpoint makes a resource ID discoverable, pair the saved credential
with a dynamic resource selector using the add-selector and validate-selector skills:
dependsOn, and give the resource
selector and its manual advanced input the same canonical parameter.For an existing integration, inspect persisted workflow serialization as well as the visible form before removing old auth fields. Establish whether existing users need a migration or compatibility path; do not add permanent legacy branches speculatively when removal is authorized. When compatibility is needed, prove that old values survive serialization and execution; hiding a field is not proof. If the user authorizes a production usage check, query only the aggregate usage/auth-shape evidence needed and keep identities, secrets, and local evidence out of commits and PRs.
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.
transformResponse against unverified payload shapesIf response schemas are missing or incomplete, do one of the following before proceeding:
apps/sim/tools/{service}/
├── index.ts # Barrel exports
├── types.ts # TypeScript interfaces
├── {action1}.ts # Tool for action 1
├── {action2}.ts # Tool for action 2
└── ...Choose the tool boundary before writing the declaration:
InternalToolConfig.operation for same-process Sim/provider work. Put the handler under
apps/sim/lib/internal/{service}/execute-tool.ts and register every ID in
apps/sim/lib/internal/tool-operations/registry.server.ts.ToolConfig.request only for an absolute external HTTP(S) provider endpoint.Never point a tool at /api/..., construct an absolute URL back to Sim, declare
request.internal, add a directExecution property (it fails bun run check:tool-request-boundary), or add an API route merely to reuse code, normalize files, or authorize
resources. A real external/browser route and an in-process tool may share the same operation, but
neither calls the other. Follow the full transport and handler rules in the add-tools skill.
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}{Action}Response extends ToolResponse {
output: {
// Define output structure
}
}Declare one response interface per tool, imported by that tool's ToolConfig<Params, Response> (or InternalToolConfig for in-process work). Never add an umbrella {Service}Response union: nothing imports it.
Tool file pattern: an external provider API uses ToolConfig with request (absolute https:// URL, headers, body, transformResponse); same-process Sim work uses InternalToolConfig with operation. Both full templates, param visibility rules, and output typing live in .agents/skills/add-tools/SKILL.md — read it before writing the first tool.
visibility: 'hidden' for OAuth tokensvisibility: 'user-only' for API keys and user credentialsvisibility: 'user-or-llm' for operation parameters?? null for nullable API response fields?? [] for optional array fieldsoptional: true for outputs that may not existtype: 'json' and you know the object shape, define properties with the inner fields so downstream consumers know the structure. Only use bare type: 'json' when the shape is truly dynamicClassify every request field (ordinary provider input / AI-consumed text / opaque model bytes /
Sim-durable storage) before implementing the tool and apply the shared projection or provenance
mechanism only where a concrete Sim {{...}} resolution path reaches a later model or log boundary.
Full rules and the required tests are in .agents/skills/add-tools/SKILL.md → "Resolved Secrets and
Provenance Boundaries".
apps/sim/blocks/blocks/{service}.ts
Follow .agents/skills/add-block/SKILL.md for the block structure, subBlock types,
condition/dependsOn/required/mode syntax, outputs, canvasPresentation sentences, and the
{Service}BlockMeta export (minimum 7 templates, plus url and skills). Every block declares
canvasPresentation; bun run apps/sim/scripts/check-canvas-sentences.ts --block={service} must
pass (CI runs check:canvas-sentences --require-coverage).
Three rules that are easy to get wrong when copying from existing blocks:
selectorKey must use the unified server selector path. Apply the add-selector skill:
add browser-safe metadata to apps/sim/lib/selectors/manifest.ts, reuse or extract a server-only
provider listing primitive, and add a credential- and destination-bound server attachment. Do not
add a client provider fetcher, a provider-specific query key, browser token acquisition, or a
selector-only API route. The shared context builder sends only active dependsOn values and
preserves exact {{KEY}} environment references for server-side resolution.canonicalParamId; its constraints are in
.claude/rules/sim-integrations.md and the add-block skill → canonicalParamId Pattern.short-input, long-input, code) and every selector declares a
placeholder; an empty box tells the user nothing. Secrets read Enter your {thing} (e.g.
Enter your API key), free text names what to type (Enter branch name), and formatted values
show the shape (2023-01-01T00:00:00Z, 1 to 1000). An optional field with a server-side default
names that default (Defaults to the database region). Dropdowns, switches, and oauth-input do
not need one.apps/sim/components/icons.tsx
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>
)
}Do not search for icons yourself. At the end of implementation, ask the user to paste the service's SVG (usually on its brand/press kit page).
Once the user provides the SVG:
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:
fill='currentColor', not fill='#fff' / fill='#000000'. It then inherits
white inside dark tiles, near-black inside light tiles (via
getTileIconColorClass), and the theme-aware var(--text-icon) bare — legible
everywhere. Do NOT set iconColor for these.iconColor (a vivid brand hex, never a
near-black/near-white tile color) if the bare icon should adopt a brand tint.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.
If the service supports webhooks or needs polling, follow .agents/skills/add-trigger/SKILL.md
(directory layout, buildTriggerSubBlocks, provider handler, polling handler); then wire
triggers.enabled / triggers.available into the block and spread each trigger's
getTrigger(id).subBlocks after the tool subBlocks.
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, ExecutableToolConfig> = {
// ... existing tools ...
{service}_action1: {service}Action1Tool,
{service}_action2: {service}Action2Tool,
}Then regenerate the generated tool metadata and commit it:
bun run generate:tool-metadataClient 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.
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,
}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,
}Do this for every integration that uses the shared credential picker. Only a connection that depends on deployment-wide OAuth client fields needs an OAuth client capability; a customer-owned service account must remain available without those fields.
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.
authMode: AuthMode.OAuth for integrations using browser OAuth or customer-owned OAuth
client credentials. Token-only service accounts can retain AuthMode.ApiKey with the shared
picker, as Coda does; oauth-input alone does not determine the authentication protocol.
Register a new token-only service ID and block type in tokenCredentialIntegrationTypes in
packages/deployment-config/src/integration-availability.ts so availability and integration
policy recognize the saved credential path.
For OAuth integrations, this value lets the catalog discover the connection flow instead of
routing "Add to Sim" to chat. Ensure the block has exactly one distinct OAuth serviceId
matching the canonical service in apps/sim/lib/oauth/oauth.ts. The canonical service's
authType selects browser OAuth or the service-account modal. Verify the resulting catalog
and block connection actions for the chosen authentication method.resolveOAuthClientCapabilityId(serviceId) resolves to the intended provider entry in
OAUTH_CLIENT_CAPABILITIES in packages/deployment-config/src/env-capabilities.ts. Google and
Microsoft service IDs deliberately share provider-level capabilities.OAUTH_CLIENT_CAPABILITIES, add
every referenced field to the env schema in apps/sim/lib/core/config/env.ts, and add the
matching text or secret entries to OAUTH_CLIENT_SETUP_FIELDS in
packages/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.serviceAccountProviderId, run
bun run generate:deployment-config to refresh
packages/deployment-config/src/service-account-providers.generated.ts; never hand-edit the
generated provider-ID map. In packages/deployment-config/src/service-account-metadata.ts, use:
deploymentRequirement when 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.
For a service-account-only default, set the canonical service's authType: 'service_account'
and serviceAccountProviderId. Verify both block availability and the connect modal with no
deployment OAuth credentials configured. An existing browser OAuth path may remain for legacy
credentials without becoming a prerequisite for the new path.Never add a permissive fallback for missing capability metadata. A browser OAuth connection without a resolvable capability must fail validation; an independent service account uses its own metadata.
Run bun run generate:tool-metadata, bun run scripts/generate-docs.ts,
bun run generate:deployment-config, then bun run check:audits (see the validate-integration
skill → Regenerate Derived Artifacts for the full list and what each check verifies).
The docs generator creates apps/docs/content/docs/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).
Every generated integration page carries a hand-written intro directly under <BlockInfoCard />. The
generator preserves it across regenerations, so write it once after the first generate:
{/* MANUAL-CONTENT-START:intro */}
[{Service}](https://service.com/) is {one sentence on what the service is}.
With the {Service} block, you can:
- **{Capability}**: {what the operations in this group do}
- **{Capability}**: {...}
{How to connect: which credential to create and where, if it is not OAuth.}
In Sim, the {Service} block lets your agents {concrete workflow uses}.
{/* MANUAL-CONTENT-END */}Group the bullets by what the user gets done, not one bullet per tool. Only describe operations the
block actually ships. Follow .claude/rules/constitution.md for voice. Re-run
bun run scripts/generate-docs.ts afterwards and confirm the section survived unchanged.
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.
If creating V2 versions (API-aligned outputs):
V2 Tools - Add _v2 suffix, version 2.0.0, flat outputs
V2 Block - Add _v2 type, use createVersionedToolSelector
V1 Block - Add (Legacy) to name, set hideFromToolbar: true, and add
sunset: { status: 'legacy', replacedBy: '{service}_v2' } — check-block-registry
fails a legacy block with no replacedBy, and the amber legacy badge plus its
click-to-upgrade action read from that field.
Only add replacedBy once the target is GA. The same check also fails when
the target is unregistered, itself sunset, or still preview: true. If v2 is
preview-gated, leave v1 alone until GA and drop preview in the same commit
that adds the sunset — splitting them breaks the build in between.
Registry - Register both versions
// In registry
{service}: {Service}Block, // V1 (legacy, hidden)
{service}_v2: {Service}V2Block, // V2 (visible)tools/{service}/ directorytypes.ts with all interfacesInternalToolConfig.operation or absolute
external HTTP(S) ToolConfig.request/api/..., constructs a URL back to Sim, declares request.internal or a
directExecution property (fails bun run check:tool-request-boundary), or has an HTTP fallback for an in-process operation?? nulloptional: trueindex.ts barrel exporttools/registry.tsbun run generate:tool-metadata and committed the regenerated artifactsbun run check:tool-request-boundary passesblocks/blocks/{service}.tsintegrationType to the correct IntegrationType enum value{Service}BlockMeta.tags lists every applicable IntegrationTag (tags live on the meta, not the block)credentialKind; browser OAuth scopes
use getScopesForService('{service}'), while variable service-account permissions stay on the credentialshort-input, long-input, code, and selector subBlock has a placeholderselectorKey exists in the shared manifest and has one server attachment with
trusted credential provider binding and a fixed, credential-bound, or explicitly reviewed
user-controlled destination policyblocks/registry-maps.ts (BLOCK_REGISTRY / BLOCK_META_REGISTRY)triggers.enabled and triggers.availablegetTrigger(){Service}BlockMeta with at least 7 templatescanvasPresentation.sentences covers every operation; bun run apps/sim/scripts/check-canvas-sentences.ts --block={service} passes{Service}BlockMeta also sets url (verified external homepage) and skills (grounded in tools.access, sourced from real use cases) — see add-block → BlockMetalib/oauth/oauth.ts under OAUTH_PROVIDERSSCOPE_DESCRIPTIONS within lib/oauth/utils.tsgetCanonicalScopesForProvider() in lib/auth/connectors/providers.ts (never hardcode)getScopesForService() for the browser OAuth permissions the block needs (never hardcode)oauth-input.serviceIdOAUTH_CLIENT_CAPABILITIES entry; independent service accounts work without deployment OAuth fieldsapps/sim/lib/core/config/env.tsOAUTH_CLIENT_CAPABILITIES; matching CLI input modes live in the exhaustively checked OAUTH_CLIENT_SETUP_FIELDSserviceAccountProviderId is configured, SERVICE_ACCOUNT_METADATA_BY_OAUTH_SERVICE_ID has the matching projection and deployment requirementcomponents/icons.tsxfill='currentColor' (not hardcoded white/black) so the icon renders bare in light AND dark mode — verified with bun run check:bare-iconstriggers/{service}/ directoryutils.ts with options, instructions, and extra fields helpersincludeDropdown: trueincludeDropdownbuildTriggerSubBlocks helperindex.ts barrel exporttriggers/registry.tsbun run scripts/generate-docs.tsbun run generate:deployment-config for OAuth or service-account changes{/* MANUAL-CONTENT-START:intro */} section under <BlockInfoCard /> and confirmed it survives a regeneratepackages/deployment-config/src/integrations.json changebun run check:integration-catalog passesbun run check:docs passes — 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 check:deployment-config passestools.config.params correctly maps and coerces all param typestransformResponse path against documented or live-verified JSON responses{Service}BlockMeta exported with at least 7 templates, each having icon, title, prompt, modules, category, and tagsWhen your integration handles file uploads or downloads, follow these patterns to work with UserFile objects consistently.
UserFile (apps/sim/executor/types.ts) is the standard file representation in Sim — id, name, an access url (not guaranteed presigned — remoteUrl is the short-lived signed one, set only for providers that fetch by URL), size, MIME type, storage key, and optional inline base64 / provider file handles. Read file bytes through the documented upload helpers, never by fetching url directly. Read the interface rather than relying on a copy here.
File authorization, normalization, storage reads, provider upload, and response mapping belong in a registered in-process operation. Do not create an internal API route for file tools.
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 the id of a subblock outside its canonical group.
tools.config.tool selects the tool before variable resolution and must not mutate or coerce input.
Use tools.config.params, which runs after variable resolution, to normalize all file variants:
import { normalizeFileInput } from '@/blocks/utils'
tools: {
config: {
tool: (params) => `{service}_${params.operation}`,
params: (params) => {
// Serialization collapses the basic/advanced pair into the canonical `file` key.
const normalizedFile = normalizeFileInput(params.file, { single: true })
return normalizedFile ? { file: normalizedFile } : {}
},
},
}export const {service}UploadTool: InternalToolConfig<Params, Response> = {
id: '{service}_upload',
// ...
params: {
file: { type: 'file', required: false, visibility: 'user-or-llm' },
},
operation: {
input: (params) => ({
accessToken: params.accessToken,
file: params.file,
}),
},
}Implement apps/sim/lib/internal/{service}/execute-tool.ts and keep the file/provider work in typed
operations beside it. The handler validates request.input, derives storage authority only from
trusted request.context, authorizes every stored file before reading bytes, forwards
request.signal, enforces declared and actual byte caps, and returns the canonical tool response.
Register {service}_upload in apps/sim/lib/internal/tool-operations/registry.server.ts; the
sweep in apps/sim/tools/request-transport.test.ts fails a forgotten registration
(registry.server.test.ts checks registered ids are canonical with loadable handlers). For anything more, run the
test-audit gate. There is no HTTP fallback.
Declare a file / file[] output on the tool. For a raw binary endpoint, set
request.responseType: 'binary' and return output.file = { name, mimeType, data: buffer, size }
from transformResponse(response, params?, context?). The executor's FileToolProcessor stores it
and replaces it with a UserFile; tools never call it.
In an operation handler, return createInternalToolFileResult / createInternalToolFilesResult
from lib/internal/tool-operations/file-result.ts — never base64 JSON. See the add-tools skill →
File Downloads and Generated Files.
| Helper | Location | Purpose |
|---|---|---|
normalizeFileInput | @/blocks/utils | Normalize file params in block config |
processFilesToUserFiles | @/lib/uploads/utils/file | Convert raw inputs to UserFile[] |
downloadFileFromStorage | @/lib/uploads/utils/file.server | Get file Buffer from UserFile |
FileToolProcessor | @/executor/utils/file-tool-processor | Executor-side; stores declared file outputs (not called by tools) |
isUserFile | @/lib/core/utils/user-file | Type guard for UserFile objects |
FileInputSchema | @/lib/uploads/utils/file-schemas | Zod schema for file validation |
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.
Use wandConfig for fields that are hard to fill out manually:
generationType: 'timestamp' to inject current date context into the AI promptgenerationType: 'json-object' for structured data{
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',
},
}Scopes are maintained in a single source of truth and reused everywhere:
lib/oauth/oauth.ts under OAUTH_PROVIDERS[provider].services[service].scopesSCOPE_DESCRIPTIONS within lib/oauth/utils.ts for the OAuth modal UIlib/auth/connectors/providers.ts (buildConnectorProviders) using getCanonicalScopesForProvider(providerId) from @/lib/oauth/utilsgetScopesForService(serviceId) from @/lib/oauth/utilsNever hardcode scope arrays in the Better Auth connector providers or block requiredScopes. Always import from the centralized source.
// In lib/auth/connectors/providers.ts (Better Auth connector providers)
scopes: getCanonicalScopesForProvider('{service}'),
// In block credential sub-block
requiredScopes: getScopesForService('{service}'),serviceId in oauth-input must match the OAuth provider configurationnew Uint8Array(buffer) for TypeScript compatibilityfileContent params - Only an existing tool that already accepted base64 fileContent keeps that hidden param; new tools take file onlysimstudioai/sim
8df65ad
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.