Automate 21risk tasks via Rube MCP (Composio). Always search tools first for current schemas.
The English summary is being prepared. Showing the original description.
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`.
The English summary is being prepared. Showing the original description.
Run score Not tested
See how to installReview the instructions the agent will receive before you install.
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-docsIf 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
└── ...
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: { /* ... */ },
}
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 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.
{{...}} 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.request.modelInput with
mode: '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 plus applyProjected; verify that selecting the
rebuilt params reproduces the projected selection.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.privateProvenance to a projected request, or use
mode: '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 call
validateOpaqueModelInputProvenance before downloading or sending content to the model and must
apply the workspace-file provenance guard before reading a persisted workspace file.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 is NULL; never invent a
tool-local migration rule.Hard rules:
executeTool boundary owns
transport and strips private metadata from functional results.directExecution. Project proven
model-visible external fields with request.modelInput; otherwise preserve ordinary request
semantics. Use an authenticated internal route when encrypted provenance must cross the boundary.{{...}} resolution path and a later model/log boundary. If an
unsupported field can resolve a secret but does not justify durable tracking (for example a
file_write path), reject it at that exact ingress.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.
apps/sim/blocks/blocks/{service}.ts
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: { /* ... */ },
}
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:
canonicalParamId must NOT match any subblock's id in the blockcanonicalParamId must be unique block-wide, not per operation. buildCanonicalIndex keys
groups by canonicalParamId across all subblocks and a group holds exactly one basicId, so two
operations that each need their own pair must use two different canonical idscanonicalParamId to 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 sources required: false, and enforce "exactly one" at executionmode only controls UI visibility, NOT serialization. Without canonicalParamId, both basic and advanced field values would be sentid must be unique within the block. Duplicate IDs cause conflicts even with different conditionsrequired: true, ALL subblocks in that group must have required: true (prevents bypassing validation by switching modes)fileId), NOT raw subblock IDs (e.g., fileSelector, manualFileId)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 BlockMeta
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 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:
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, create triggers using the generic buildTriggerSubBlocks helper.
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)
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'),
}),
// ...
}
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.
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:generate
Client 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 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.
serviceId and that it matches the canonical
service entry in apps/sim/lib/oauth/oauth.ts.resolveOAuthClientCapabilityId(serviceId) resolves to the intended provider entry in
OAUTH_CLIENT_CAPABILITIES in apps/sim/lib/core/config/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
scripts/setup/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, add the matching projection to
SERVICE_ACCOUNT_METADATA_BY_OAUTH_SERVICE_ID in
apps/sim/lib/integrations/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.Never add a permissive fallback for missing capability metadata. A visible OAuth integration without a resolvable capability must fail validation.
Run the documentation generator:
bun run scripts/generate-docs.ts
bun run integration-catalog:check
This 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 same generator refreshes apps/sim/lib/integrations/integrations.json. The catalog check then
derives the deployment-relevant fields from the executable block registry and compares them with the
committed projection. Review the generated diff and keep only intentional changes.
If creating V2 versions (API-aligned outputs):
_v2 suffix, version 2.0.0, flat outputs_v2 type, use createVersionedToolSelector(Legacy) to name, set hideFromToolbar: true// In registry
{service}: {Service}Block, // V1 (legacy, hidden)
{service}_v2: {Service}V2Block, // V2 (visible)
tools/{service}/ directorytypes.ts with all interfaces?? nulloptional: trueindex.ts barrel exporttools/registry.tsbun run tool-metadata:generate and committed the regenerated artifactsblocks/blocks/{service}.tsintegrationType to the correct IntegrationType enum valuetags array with all applicable IntegrationTag valuesrequiredScopes: getScopesForService('{service}')blocks/registry-maps.ts (BLOCK_REGISTRY / BLOCK_META_REGISTRY)triggers.enabled and triggers.availablegetTrigger(){Service}BlockMeta with at least 7 templateslib/oauth/oauth.ts under OAUTH_PROVIDERSSCOPE_DESCRIPTIONS within lib/oauth/utils.tsgetCanonicalScopesForProvider() in auth.ts (never hardcode)getScopesForService() in block requiredScopes (never hardcode)oauth-input.serviceIdresolveOAuthClientCapabilityId(serviceId) resolves to the intended OAUTH_CLIENT_CAPABILITIES entryapps/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.tsapps/sim/lib/integrations/integrations.json changebun run integration-catalog:check 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 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.
When your integration handles file uploads or downloads, follow these patterns to work with UserFile objects consistently.
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
}
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.
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.
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}`
},
},
}
Create apps/sim/app/api/tools/{service}/{action}/route.ts. 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.
// 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
})
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,
}),
},
}
For tools that return files, use FileToolProcessor to store files and return UserFile objects.
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 },
}
}
// Return file data that FileToolProcessor can handle
return NextResponse.json({
success: true,
output: {
file: {
data: base64Content,
mimeType: 'application/pdf',
filename: 'document.pdf',
},
},
})
| 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 |
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 UIgetCanonicalScopesForProvider(providerId) from @/lib/oauth/utilsgetScopesForService(serviceId) from @/lib/oauth/utilsNever 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}'),
serviceId in oauth-input must match the OAuth provider configurationstripe_create_payment, not stripeCreatePayment. This applies to tool id fields, registry keys, tools.access arrays, and tools.config.tool return valuestype: 'stripe', not type: 'Stripe'required: { field: 'op', value: 'create' } instead of always truenew Uint8Array(buffer) for TypeScript compatibilityfileContent params for backwards compatibilitymode: 'advanced' on rarely-used optional fieldswandConfig enabledgetScopesForService() in blocks and getCanonicalScopesForProvider() in auth.tsSCOPE_DESCRIPTIONS within lib/oauth/utils.tsOAUTH_CLIENT_CAPABILITIES; shared Google/Microsoft aliases map to their provider capabilityenv-capabilities.ts; CLI input modes live in the exhaustively checked scripts/setup/capability-config.ts mappingNo reviews yet. Tell others what you thought.
No login needed. Share how it worked for you, ask a question, or note where it did not run.
No comments yet. Be the first to write one.
Automate 21risk tasks via Rube MCP (Composio). Always search tools first for current schemas.
The English summary is being prepared. Showing the original description.
Automate 2chat tasks via Rube MCP (Composio). Always search tools first for current schemas.
The English summary is being prepared. Showing the original description.
Set up and use 1Password CLI for sign-in, desktop integration, and reading or injecting secrets.
The English summary is being prepared. Showing the original description.
Build integrated IS/BS/CF financial workbooks in Excel.
The English summary is being prepared. Showing the original description.
Entry point for 9Router — local/remote AI gateway with OpenAI-compatible REST for chat, image, TTS, embeddings, web search, web fetch. Use when the user mentions 9Router, NINEROUTER_URL, or wants AI without writing provider boilerplate. This skill covers setup + indexes capability skills; fetch the relevant capability SKILL.md from the URLs below when needed.
The English summary is being prepared. Showing the original description.
Chat / code generation via 9Router using OpenAI /v1/chat/completions or Anthropic /v1/messages format with streaming + auto-fallback combos. Use when the user wants to ask an LLM, generate code, summarize text, or run prompts through 9Router.
The English summary is being prepared. Showing the original description.