Compare commits

..

2 Commits

Author SHA1 Message Date
Siddharth Ganesan
3ab9b91445 Skip streaming params on load 2026-01-14 10:03:25 -08:00
Siddharth Ganesan
a36bdd8729 Clear streaming flags on load 2026-01-14 09:58:57 -08:00
5846 changed files with 118647 additions and 942275 deletions

View File

@@ -20,7 +20,6 @@ When the user asks you to create a block:
import { {ServiceName}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const {ServiceName}Block: BlockConfig = {
type: '{service}', // snake_case identifier
@@ -116,17 +115,12 @@ export const {ServiceName}Block: BlockConfig = {
id: 'credential',
title: 'Account',
type: 'oauth-input',
serviceId: '{service}', // Must match OAuth provider service key
requiredScopes: getScopesForService('{service}'), // Import from @/lib/oauth/utils
serviceId: '{service}', // Must match OAuth provider
placeholder: 'Select account',
required: true,
}
```
**Scopes:** Always use `getScopesForService(serviceId)` from `@/lib/oauth/utils` for `requiredScopes`. Never hardcode scope arrays — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`.
**Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`.
### Selectors (with dynamic options)
```typescript
// Channel selector (Slack, Discord, etc.)
@@ -189,109 +183,6 @@ export const {ServiceName}Block: BlockConfig = {
}
```
## File Input Handling
When your block accepts file uploads, use the basic/advanced mode pattern with `normalizeFileInput`.
### Basic/Advanced File Pattern
```typescript
// Basic mode: Visual file upload
{
id: 'uploadFile',
title: 'File',
type: 'file-upload',
canonicalParamId: 'file', // Both map to 'file' param
placeholder: 'Upload file',
mode: 'basic',
multiple: false,
required: true,
condition: { field: 'operation', value: 'upload' },
},
// Advanced mode: Reference from other blocks
{
id: 'fileRef',
title: 'File',
type: 'short-input',
canonicalParamId: 'file', // Both map to 'file' param
placeholder: 'Reference file (e.g., {{file_block.output}})',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'upload' },
},
```
**Critical constraints:**
- `canonicalParamId` must NOT match any subblock's `id` in the same block
- Values are stored under subblock `id`, not `canonicalParamId`
### Normalizing File Input in tools.config
Use `normalizeFileInput` to handle all input variants:
```typescript
import { normalizeFileInput } from '@/blocks/utils'
tools: {
access: ['service_upload'],
config: {
tool: (params) => {
// Check all field IDs: uploadFile (basic), fileRef (advanced), fileContent (legacy)
const normalizedFile = normalizeFileInput(
params.uploadFile || params.fileRef || params.fileContent,
{ single: true }
)
if (normalizedFile) {
params.file = normalizedFile
}
return `service_${params.operation}`
},
},
}
```
**Why this pattern?**
- Values come through as `params.uploadFile` or `params.fileRef` (the subblock IDs)
- `canonicalParamId` only controls UI/schema mapping, not runtime values
- `normalizeFileInput` handles JSON strings from advanced mode template resolution
### File Input Types in `inputs`
Use `type: 'json'` for file inputs:
```typescript
inputs: {
uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' },
fileRef: { type: 'json', description: 'File reference from previous block' },
// Legacy field for backwards compatibility
fileContent: { type: 'string', description: 'Legacy: base64 encoded content' },
}
```
### Multiple Files
For multiple file uploads:
```typescript
{
id: 'attachments',
title: 'Attachments',
type: 'file-upload',
multiple: true, // Allow multiple files
maxSize: 25, // Max size in MB per file
acceptedTypes: 'image/*,application/pdf,.doc,.docx',
}
// In tools.config:
const normalizedFiles = normalizeFileInput(
params.attachments || params.attachmentRefs,
// No { single: true } - returns array
)
if (normalizedFiles) {
params.files = normalizedFiles
}
```
## Condition Syntax
Controls when a field is shown based on other field values.
@@ -460,18 +351,14 @@ Enables AI-assisted field generation.
## Tools Configuration
**Important:** `tools.config.tool` runs during serialization before variable resolution. Put `Number()` and other type coercions in `tools.config.params` instead, which runs at execution time after variables are resolved.
**Preferred:** Use tool names directly as dropdown option IDs to avoid switch cases:
### Simple Tool Selector
```typescript
// Dropdown options use tool IDs directly
options: [
{ label: 'Create', id: 'service_create' },
{ label: 'Read', id: 'service_read' },
]
// Tool selector just returns the operation value
tool: (params) => params.operation,
tools: {
access: ['service_create', 'service_read', 'service_update'],
config: {
tool: (params) => `service_${params.operation}`,
},
}
```
### With Parameter Transformation
@@ -538,41 +425,6 @@ outputs: {
}
```
### Typed JSON Outputs
When using `type: 'json'` and you know the object shape in advance, **describe the inner fields in the description** so downstream blocks know what properties are available. For well-known, stable objects, use nested output definitions instead:
```typescript
outputs: {
// BAD: Opaque json with no info about what's inside
plan: { type: 'json', description: 'Zone plan information' },
// GOOD: Describe the known fields in the description
plan: {
type: 'json',
description: 'Zone plan information (id, name, price, currency, frequency, is_subscribed)',
},
// BEST: Use nested output definition when the shape is stable and well-known
plan: {
id: { type: 'string', description: 'Plan identifier' },
name: { type: 'string', description: 'Plan name' },
price: { type: 'number', description: 'Plan price' },
currency: { type: 'string', description: 'Price currency' },
},
}
```
Use the nested pattern when:
- The object has a small, stable set of fields (< 10)
- Downstream blocks will commonly access specific properties
- The API response shape is well-documented and unlikely to change
Use `type: 'json'` with a descriptive string when:
- The object has many fields or a dynamic shape
- It represents a list/array of items
- The shape varies by operation
## V2 Block Pattern
When creating V2 blocks (alongside legacy V1):
@@ -630,7 +482,6 @@ export const registry: Record<string, BlockConfig> = {
import { ServiceIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const ServiceBlock: BlockConfig = {
type: 'service',
@@ -661,7 +512,6 @@ export const ServiceBlock: BlockConfig = {
title: 'Service Account',
type: 'oauth-input',
serviceId: 'service',
requiredScopes: getScopesForService('service'),
placeholder: 'Select account',
required: true,
},
@@ -727,99 +577,15 @@ export const ServiceBlock: BlockConfig = {
See the `/add-trigger` skill for creating triggers.
## Icon Requirement
If the icon doesn't already exist in `@/components/icons.tsx`, **do NOT search for it yourself**. After completing the block, ask the user to provide the SVG:
```
The block is complete, but I need an icon for {Service}.
Please provide the SVG and I'll convert it to a React component.
You can usually find this in the service's brand/press kit page, or copy it from their website.
```
## 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. This includes:
- Pagination tokens
- Time range filters (start/end time)
- Sort order options
- Reply settings
- Rarely used IDs (e.g., reply-to tweet ID, quote tweet ID)
- Max results / limits
```typescript
{
id: 'startTime',
title: 'Start Time',
type: 'short-input',
placeholder: 'ISO 8601 timestamp',
condition: { field: 'operation', value: ['search', 'list'] },
mode: 'advanced', // Rarely used, hide from basic view
}
```
## WandConfig for Complex Inputs
Use `wandConfig` for fields that are hard to fill out manually, such as timestamps, comma-separated lists, and complex query strings. This gives users an AI-assisted input experience.
```typescript
// Timestamps - use generationType: 'timestamp' to inject current date context
{
id: 'startTime',
title: 'Start Time',
type: 'short-input',
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: 'Generate an ISO 8601 timestamp based on the user description. Return ONLY the timestamp string.',
generationType: 'timestamp',
},
}
// Comma-separated lists - simple prompt without generationType
{
id: 'mediaIds',
title: 'Media IDs',
type: 'short-input',
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: 'Generate a comma-separated list of media IDs. Return ONLY the comma-separated values.',
},
}
```
## Naming Convention
All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MUST use `snake_case` (e.g., `x_create_tweet`, `slack_send_message`). Never use camelCase or PascalCase.
## Checklist Before Finishing
- [ ] All subBlocks have `id`, `title` (except switch), and `type`
- [ ] Conditions use correct syntax (field, value, not, and)
- [ ] DependsOn set for fields that need other values
- [ ] Required fields marked correctly (boolean or condition)
- [ ] OAuth inputs have correct `serviceId` and `requiredScopes: getScopesForService(serviceId)`
- [ ] Scope descriptions added to `SCOPE_DESCRIPTIONS` in `lib/oauth/utils.ts` for any new scopes
- [ ] Tools.access lists all tool IDs (snake_case)
- [ ] Tools.config.tool returns correct tool ID (snake_case)
- [ ] OAuth inputs have correct `serviceId`
- [ ] Tools.access lists all tool IDs
- [ ] Tools.config.tool returns correct tool ID
- [ ] Outputs match tool outputs
- [ ] Block registered in registry.ts
- [ ] If icon missing: asked user to provide SVG
- [ ] If triggers exist: `triggers` config set, trigger subBlocks spread
- [ ] Optional/rarely-used fields set to `mode: 'advanced'`
- [ ] Timestamps and complex inputs have `wandConfig` enabled
## Final Validation (Required)
After creating the block, you MUST validate it against every tool it references:
1. **Read every tool definition** that appears in `tools.access` — do not skip any
2. **For each tool, verify the block has correct:**
- SubBlock inputs that cover all required tool params (with correct `condition` to show for that operation)
- SubBlock input types that match the tool param types (e.g., dropdown for enums, short-input for strings)
- `tools.config.params` correctly maps subBlock IDs to tool param names (if they differ)
- Type coercions in `tools.config.params` for any params that need conversion (Number(), Boolean(), JSON.parse())
3. **Verify block outputs** cover the key fields returned by all tools
4. **Verify conditions** — each subBlock should only show for the operations that actually use it

View File

@@ -1,299 +0,0 @@
---
description: Add a knowledge base connector for syncing documents from an external source
argument-hint: <service-name> [api-docs-url]
---
# Add Connector Skill
You are an expert at adding knowledge base connectors to Sim. A connector syncs documents from an external source (Confluence, Google Drive, Notion, etc.) into a knowledge base.
## Your Task
When the user asks you to create a connector:
1. Use Context7 or WebFetch to read the service's API documentation
2. Determine the auth mode: **OAuth** (if Sim already has an OAuth provider for the service) or **API key** (if the service uses API key / Bearer token auth)
3. Create the connector directory and config
4. Register it in the connector registry
## Directory Structure
Create files in `apps/sim/connectors/{service}/`:
```
connectors/{service}/
├── index.ts # Barrel export
└── {service}.ts # ConnectorConfig definition
```
## Authentication
Connectors use a discriminated union for auth config (`ConnectorAuthConfig` in `connectors/types.ts`):
```typescript
type ConnectorAuthConfig =
| { mode: 'oauth'; provider: OAuthService; requiredScopes?: string[] }
| { mode: 'apiKey'; label?: string; placeholder?: string }
```
### OAuth mode
For services with existing OAuth providers in `apps/sim/lib/oauth/types.ts`. The `provider` must match an `OAuthService`. The modal shows a credential picker and handles token refresh automatically.
### API key mode
For services that use API key / Bearer token auth. The modal shows a password input with the configured `label` and `placeholder`. The API key is encrypted at rest using AES-256-GCM and stored in a dedicated `encryptedApiKey` column on the connector record. The sync engine decrypts it automatically — connectors receive the raw access token in `listDocuments`, `getDocument`, and `validateConfig`.
## ConnectorConfig Structure
### OAuth connector example
```typescript
import { createLogger } from '@sim/logger'
import { {Service}Icon } from '@/components/icons'
import { fetchWithRetry } from '@/lib/knowledge/documents/utils'
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
const logger = createLogger('{Service}Connector')
export const {service}Connector: ConnectorConfig = {
id: '{service}',
name: '{Service}',
description: 'Sync documents from {Service} into your knowledge base',
version: '1.0.0',
icon: {Service}Icon,
auth: {
mode: 'oauth',
provider: '{service}', // Must match OAuthService in lib/oauth/types.ts
requiredScopes: ['read:...'],
},
configFields: [
// Rendered dynamically by the add-connector modal UI
// Supports 'short-input' and 'dropdown' types
],
listDocuments: async (accessToken, sourceConfig, cursor) => {
// Paginate via cursor, extract text, compute SHA-256 hash
// Return { documents: ExternalDocument[], nextCursor?, hasMore }
},
getDocument: async (accessToken, sourceConfig, externalId) => {
// Return ExternalDocument or null
},
validateConfig: async (accessToken, sourceConfig) => {
// Return { valid: true } or { valid: false, error: 'message' }
},
// Optional: map source metadata to semantic tag keys (translated to slots by sync engine)
mapTags: (metadata) => {
// Return Record<string, unknown> with keys matching tagDefinitions[].id
},
}
```
### API key connector example
```typescript
export const {service}Connector: ConnectorConfig = {
id: '{service}',
name: '{Service}',
description: 'Sync documents from {Service} into your knowledge base',
version: '1.0.0',
icon: {Service}Icon,
auth: {
mode: 'apiKey',
label: 'API Key', // Shown above the input field
placeholder: 'Enter your {Service} API key', // Input placeholder
},
configFields: [ /* ... */ ],
listDocuments: async (accessToken, sourceConfig, cursor) => { /* ... */ },
getDocument: async (accessToken, sourceConfig, externalId) => { /* ... */ },
validateConfig: async (accessToken, sourceConfig) => { /* ... */ },
}
```
## ConfigField Types
The add-connector modal renders these automatically — no custom UI needed.
```typescript
// Text input
{
id: 'domain',
title: 'Domain',
type: 'short-input',
placeholder: 'yoursite.example.com',
required: true,
}
// Dropdown (static options)
{
id: 'contentType',
title: 'Content Type',
type: 'dropdown',
required: false,
options: [
{ label: 'Pages only', id: 'page' },
{ label: 'Blog posts only', id: 'blogpost' },
{ label: 'All content', id: 'all' },
],
}
```
## ExternalDocument Shape
Every document returned from `listDocuments`/`getDocument` must include:
```typescript
{
externalId: string // Source-specific unique ID
title: string // Document title
content: string // Extracted plain text
mimeType: 'text/plain' // Always text/plain (content is extracted)
contentHash: string // SHA-256 of content (change detection)
sourceUrl?: string // Link back to original (stored on document record)
metadata?: Record<string, unknown> // Source-specific data (fed to mapTags)
}
```
## Content Hashing (Required)
The sync engine uses content hashes for change detection:
```typescript
async function computeContentHash(content: string): Promise<string> {
const data = new TextEncoder().encode(content)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
}
```
## tagDefinitions — Declared Tag Definitions
Declare which tags the connector populates using semantic IDs. Shown in the add-connector modal as opt-out checkboxes.
On connector creation, slots are **dynamically assigned** via `getNextAvailableSlot` — connectors never hardcode slot names.
```typescript
tagDefinitions: [
{ id: 'labels', displayName: 'Labels', fieldType: 'text' },
{ id: 'version', displayName: 'Version', fieldType: 'number' },
{ id: 'lastModified', displayName: 'Last Modified', fieldType: 'date' },
],
```
Each entry has:
- `id`: Semantic key matching a key returned by `mapTags` (e.g. `'labels'`, `'version'`)
- `displayName`: Human-readable name shown in the UI (e.g. "Labels", "Last Modified")
- `fieldType`: `'text'` | `'number'` | `'date'` | `'boolean'` — determines which slot pool to draw from
Users can opt out of specific tags in the modal. Disabled IDs are stored in `sourceConfig.disabledTagIds`.
The assigned mapping (`semantic id → slot`) is stored in `sourceConfig.tagSlotMapping`.
## mapTags — Metadata to Semantic Keys
Maps source metadata to semantic tag keys. Required if `tagDefinitions` is set.
The sync engine calls this automatically and translates semantic keys to actual DB slots
using the `tagSlotMapping` stored on the connector.
Return keys must match the `id` values declared in `tagDefinitions`.
```typescript
mapTags: (metadata: Record<string, unknown>): Record<string, unknown> => {
const result: Record<string, unknown> = {}
// Validate arrays before casting — metadata may be malformed
const labels = Array.isArray(metadata.labels) ? (metadata.labels as string[]) : []
if (labels.length > 0) result.labels = labels.join(', ')
// Validate numbers — guard against NaN
if (metadata.version != null) {
const num = Number(metadata.version)
if (!Number.isNaN(num)) result.version = num
}
// Validate dates — guard against Invalid Date
if (typeof metadata.lastModified === 'string') {
const date = new Date(metadata.lastModified)
if (!Number.isNaN(date.getTime())) result.lastModified = date
}
return result
}
```
## External API Calls — Use `fetchWithRetry`
All external API calls must use `fetchWithRetry` from `@/lib/knowledge/documents/utils` instead of raw `fetch()`. This provides exponential backoff with retries on 429/502/503/504 errors. It returns a standard `Response` — all `.ok`, `.json()`, `.text()` checks work unchanged.
For `validateConfig` (user-facing, called on save), pass `VALIDATE_RETRY_OPTIONS` to cap wait time at ~7s. Background operations (`listDocuments`, `getDocument`) use the built-in defaults (5 retries, ~31s max).
```typescript
import { VALIDATE_RETRY_OPTIONS, fetchWithRetry } from '@/lib/knowledge/documents/utils'
// Background sync — use defaults
const response = await fetchWithRetry(url, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
})
// validateConfig — tighter retry budget
const response = await fetchWithRetry(url, { ... }, VALIDATE_RETRY_OPTIONS)
```
## sourceUrl
If `ExternalDocument.sourceUrl` is set, the sync engine stores it on the document record. Always construct the full URL (not a relative path).
## Sync Engine Behavior (Do Not Modify)
The sync engine (`lib/knowledge/connectors/sync-engine.ts`) is connector-agnostic. It:
1. Calls `listDocuments` with pagination until `hasMore` is false
2. Compares `contentHash` to detect new/changed/unchanged documents
3. Stores `sourceUrl` and calls `mapTags` on insert/update automatically
4. Handles soft-delete of removed documents
5. Resolves access tokens automatically — OAuth tokens are refreshed, API keys are decrypted from the `encryptedApiKey` column
You never need to modify the sync engine when adding a connector.
## Icon
The `icon` field on `ConnectorConfig` is used throughout the UI — in the connector list, the add-connector modal, and as the document icon in the knowledge base table (replacing the generic file type icon for connector-sourced documents). The icon is read from `CONNECTOR_REGISTRY[connectorType].icon` at runtime — no separate icon map to maintain.
If the service already has an icon in `apps/sim/components/icons.tsx` (from a tool integration), reuse it. Otherwise, ask the user to provide the SVG.
## Registering
Add one line to `apps/sim/connectors/registry.ts`:
```typescript
import { {service}Connector } from '@/connectors/{service}'
export const CONNECTOR_REGISTRY: ConnectorRegistry = {
// ... existing connectors ...
{service}: {service}Connector,
}
```
## Reference Implementations
- **OAuth**: `apps/sim/connectors/confluence/confluence.ts` — multiple config field types, `mapTags`, label fetching
- **API key**: `apps/sim/connectors/fireflies/fireflies.ts` — GraphQL API with Bearer token auth
## Checklist
- [ ] Created `connectors/{service}/{service}.ts` with full ConnectorConfig
- [ ] Created `connectors/{service}/index.ts` barrel export
- [ ] **Auth configured correctly:**
- OAuth: `auth.provider` matches an existing `OAuthService` in `lib/oauth/types.ts`
- API key: `auth.label` and `auth.placeholder` set appropriately
- [ ] `listDocuments` handles pagination and computes content hashes
- [ ] `sourceUrl` set on each ExternalDocument (full URL, not relative)
- [ ] `metadata` includes source-specific data for tag mapping
- [ ] `tagDefinitions` declared for each semantic key returned by `mapTags`
- [ ] `mapTags` implemented if source has useful metadata (labels, dates, versions)
- [ ] `validateConfig` verifies the source is accessible
- [ ] All external API calls use `fetchWithRetry` (not raw `fetch`)
- [ ] All optional config fields validated in `validateConfig`
- [ ] Icon exists in `components/icons.tsx` (or asked user to provide SVG)
- [ ] Registered in `connectors/registry.ts`

View File

@@ -102,7 +102,6 @@ export const {service}{Action}Tool: ToolConfig<Params, Response> = {
- Always use `?? []` for optional array fields
- Set `optional: true` for outputs that may not exist
- Never output raw JSON dumps - extract meaningful fields
- When using `type: '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 dynamic
## Step 3: Create Block
@@ -114,7 +113,6 @@ export const {service}{Action}Tool: ToolConfig<Params, Response> = {
import { {Service}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { getScopesForService } from '@/lib/oauth/utils'
export const {Service}Block: BlockConfig = {
type: '{service}',
@@ -145,7 +143,6 @@ export const {Service}Block: BlockConfig = {
title: '{Service} Account',
type: 'oauth-input',
serviceId: '{service}',
requiredScopes: getScopesForService('{service}'),
required: true,
},
// Conditional fields per operation
@@ -209,15 +206,10 @@ export const {Service}Block: BlockConfig = {
}
```
**Critical Canonical Param Rules:**
- `canonicalParamId` must NOT match any subblock's `id` in the block
- `canonicalParamId` must be unique per operation/condition context
- Only use `canonicalParamId` to link basic/advanced alternatives for the same logical parameter
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent
- Every subblock `id` must 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 have `required: 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)
**Critical:**
- `canonicalParamId` must NOT match any other subblock's `id`, must be unique per block, and should only be used to link basic/advanced alternatives for the same parameter.
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent.
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions.
## Step 4: Add Icon
@@ -234,26 +226,17 @@ export function {Service}Icon(props: SVGProps<SVGSVGElement>) {
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* SVG paths from user-provided SVG */}
{/* SVG paths from brand assets */}
</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:
1. Extract the SVG paths/content
2. Create a React component that spreads props
3. Ensure viewBox is preserved from the original SVG
### Finding Icons
1. Check the service's brand/press kit page
2. Download SVG logo
3. Convert to React component
4. Ensure it accepts and spreads props
## Step 5: Create Triggers (Optional)
@@ -411,7 +394,7 @@ If creating V2 versions (API-aligned outputs):
### Block
- [ ] Created `blocks/blocks/{service}.ts`
- [ ] Defined operation dropdown with all operations
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
- [ ] Added credential field (oauth-input or short-input)
- [ ] Added conditional fields per operation
- [ ] Set up dependsOn for cascading selectors
- [ ] Configured tools.access with all tool IDs
@@ -421,14 +404,7 @@ If creating V2 versions (API-aligned outputs):
- [ ] If triggers: set `triggers.enabled` and `triggers.available`
- [ ] If triggers: spread trigger subBlocks with `getTrigger()`
### OAuth Scopes (if OAuth service)
- [ ] Defined scopes in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS`
- [ ] Added scope descriptions in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
- [ ] Used `getCanonicalScopesForProvider()` in `auth.ts` (never hardcode)
- [ ] Used `getScopesForService()` in block `requiredScopes` (never hardcode)
### Icon
- [ ] Asked user to provide SVG
- [ ] Added icon to `components/icons.tsx`
- [ ] Icon spreads props correctly
@@ -445,12 +421,6 @@ If creating V2 versions (API-aligned outputs):
- [ ] Ran `bun run scripts/generate-docs.ts`
- [ ] Verified docs file created
### 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.params` correctly maps and coerces all param types
## Example Command
When the user asks to add an integration:
@@ -463,298 +433,18 @@ 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
4. Add the Stripe icon
5. Register everything
6. Generate docs
[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:
```typescript
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:
```typescript
// 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:
```typescript
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 Internal API Route
Create `apps/sim/app/api/tools/{service}/{action}/route.ts`:
```typescript
import { createLogger } from '@sim/logger'
import { NextResponse, type NextRequest } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { FileInputSchema, 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')
const RequestSchema = z.object({
accessToken: z.string(),
file: FileInputSchema.optional().nullable(),
// Legacy field for backwards compatibility
fileContent: z.string().optional().nullable(),
// ... other params
})
export async function POST(request: NextRequest) {
const requestId = generateRequestId()
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const data = RequestSchema.parse(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
```typescript
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
```typescript
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)
```typescript
// 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
```typescript
{
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:
1. **Define scopes** in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS[provider].services[service].scopes`
2. **Add descriptions** in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts` for the OAuth modal UI
3. **Reference in auth.ts** using `getCanonicalScopesForProvider(providerId)` from `@/lib/oauth/utils`
4. **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.
```typescript
// In auth.ts (Better Auth config)
scopes: getCanonicalScopesForProvider('{service}'),
// In block credential sub-block
requiredScopes: getScopesForService('{service}'),
```
### Common Gotchas
## Common Gotchas
1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration
2. **All tool IDs MUST be snake_case** - `stripe_create_payment`, not `stripeCreatePayment`. This applies to tool `id` fields, registry keys, `tools.access` arrays, and `tools.config.tool` return values
2. **Tool IDs are snake_case** - `stripe_create_payment`, not `stripeCreatePayment`
3. **Block type is snake_case** - `type: 'stripe'`, not `type: 'Stripe'`
4. **Alphabetical ordering** - Keep imports and registry entries alphabetically sorted
5. **Required can be conditional** - Use `required: { field: 'op', value: 'create' }` instead of always true
6. **DependsOn clears options** - When a dependency changes, selector options are refetched
7. **Never pass Buffer directly to fetch** - Convert to `new Uint8Array(buffer)` for TypeScript compatibility
8. **Always handle legacy file params** - Keep hidden `fileContent` params for backwards compatibility
9. **Optional fields use advanced mode** - Set `mode: 'advanced'` on rarely-used optional fields
10. **Complex inputs need wandConfig** - Timestamps, JSON arrays, and other hard-to-type values should have `wandConfig` enabled
11. **Never hardcode scopes** - Use `getScopesForService()` in blocks and `getCanonicalScopesForProvider()` in auth.ts
12. **Always add scope descriptions** - New scopes must have entries in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`

View File

@@ -55,21 +55,21 @@ export const {serviceName}{Action}Tool: ToolConfig<
},
params: {
// Hidden params (system-injected, only use hidden for oauth accessToken)
// Hidden params (system-injected)
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
// User-only params (credentials, api key, IDs user must provide)
// User-only params (credentials, IDs user must provide)
someId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the resource',
},
// User-or-LLM params (everything else, can be provided by user OR computed by LLM)
// User-or-LLM params (can be provided by user OR computed by LLM)
query: {
type: 'string',
required: false, // Use false for optional
@@ -114,8 +114,8 @@ export const {serviceName}{Action}Tool: ToolConfig<
### Visibility Options
- `'hidden'` - System-injected (OAuth tokens, internal params). User never sees.
- `'user-only'` - User must provide (credentials, api keys, account-specific IDs)
- `'user-or-llm'` - User provides OR LLM can compute (search queries, content, filters, most fall into this category)
- `'user-only'` - User must provide (credentials, account-specific IDs)
- `'user-or-llm'` - User provides OR LLM can compute (search queries, content, filters)
### Parameter Types
- `'string'` - Text values
@@ -147,18 +147,9 @@ closedAt: {
},
```
### Typed JSON Outputs
When using `type: 'json'` and you know the object shape in advance, **always define the inner structure** using `properties` so downstream consumers know what fields are available:
### Nested Properties
For complex outputs, define nested structure:
```typescript
// BAD: Opaque json with no info about what's inside
metadata: {
type: 'json',
description: 'Response metadata',
},
// GOOD: Define the known properties
metadata: {
type: 'json',
description: 'Response metadata',
@@ -168,10 +159,7 @@ metadata: {
count: { type: 'number', description: 'Total count' },
},
},
```
For arrays of objects, define the item structure:
```typescript
items: {
type: 'array',
description: 'List of items',
@@ -185,8 +173,6 @@ items: {
},
```
Only use bare `type: 'json'` without `properties` when the shape is truly dynamic or unknown.
## Critical Rules for transformResponse
### Handle Nullable Fields
@@ -286,13 +272,8 @@ If creating V2 tools (API-aligned outputs), use `_v2` suffix:
- Version: `'2.0.0'`
- Outputs: Flat, API-aligned (no content/metadata wrapper)
## Naming Convention
All tool IDs MUST use `snake_case`: `{service}_{action}` (e.g., `x_create_tweet`, `slack_send_message`). Never use camelCase or PascalCase for tool IDs.
## Checklist Before Finishing
- [ ] All tool IDs use snake_case
- [ ] All params have explicit `required: true` or `required: false`
- [ ] All params have appropriate `visibility`
- [ ] All nullable response fields use `?? null`
@@ -300,22 +281,4 @@ All tool IDs MUST use `snake_case`: `{service}_{action}` (e.g., `x_create_tweet`
- [ ] No raw JSON dumps in outputs
- [ ] Types file has all interfaces
- [ ] Index.ts exports all tools
## Final Validation (Required)
After creating all tools, you MUST validate every tool before finishing:
1. **Read every tool file** you created — do not skip any
2. **Cross-reference with the API docs** to verify:
- All required params are marked `required: true`
- All optional params are marked `required: false`
- Param types match the API (string, number, boolean, json)
- Request URL, method, headers, and body match the API spec
- `transformResponse` extracts the correct fields from the API response
- All output fields match what the API actually returns
- No fields are missing from outputs that the API provides
- No extra fields are defined in outputs that the API doesn't return
3. **Verify consistency** across tools:
- Shared types in `types.ts` match all tools that use them
- Tool IDs in the barrel export match the tool file definitions
- Error handling is consistent (error checks, meaningful messages)
- [ ] Tool IDs use snake_case

View File

@@ -1,289 +0,0 @@
---
description: Validate an existing Sim integration (tools, block, registry) against the service's API docs
argument-hint: <service-name> [api-docs-url]
---
# Validate Integration Skill
You are an expert auditor for Sim integrations. Your job is to thoroughly validate that an existing integration is correct, complete, and follows all conventions.
## Your Task
When the user asks you to validate an integration:
1. Read the service's API documentation (via WebFetch or Context7)
2. Read every tool, the block, and registry entries
3. Cross-reference everything against the API docs and Sim conventions
4. Report all issues found, grouped by severity (critical, warning, suggestion)
5. Fix all issues after reporting them
## Step 1: Gather All Files
Read **every** file for the integration — do not skip any:
```
apps/sim/tools/{service}/ # All tool files, types.ts, index.ts
apps/sim/blocks/blocks/{service}.ts # Block definition
apps/sim/tools/registry.ts # Tool registry entries for this service
apps/sim/blocks/registry.ts # Block registry entry for this service
apps/sim/components/icons.tsx # Icon definition
apps/sim/lib/auth/auth.ts # OAuth config — should use getCanonicalScopesForProvider()
apps/sim/lib/oauth/oauth.ts # OAuth provider config — single source of truth for scopes
apps/sim/lib/oauth/utils.ts # Scope utilities, SCOPE_DESCRIPTIONS for modal UI
```
## Step 2: Pull API Documentation
Fetch the official API docs for the service. This is the **source of truth** for:
- Endpoint URLs, HTTP methods, and auth headers
- Required vs optional parameters
- Parameter types and allowed values
- Response shapes and field names
- Pagination patterns (which param name, which response field)
- Rate limits and error formats
## Step 3: Validate Tools
For **every** tool file, check:
### Tool ID and Naming
- [ ] Tool ID uses `snake_case`: `{service}_{action}` (e.g., `x_create_tweet`, `slack_send_message`)
- [ ] Tool `name` is human-readable (e.g., `'X Create Tweet'`)
- [ ] Tool `description` is a concise one-liner describing what it does
- [ ] Tool `version` is set (`'1.0.0'` or `'2.0.0'` for V2)
### Params
- [ ] All required API params are marked `required: true`
- [ ] All optional API params are marked `required: false`
- [ ] Every param has explicit `required: true` or `required: false` — never omitted
- [ ] Param types match the API (`'string'`, `'number'`, `'boolean'`, `'json'`)
- [ ] Visibility is correct:
- `'hidden'` — ONLY for OAuth access tokens and system-injected params
- `'user-only'` — for API keys, credentials, and account-specific IDs the user must provide
- `'user-or-llm'` — for everything else (search queries, content, filters, IDs that could come from other blocks)
- [ ] Every param has a `description` that explains what it does
### Request
- [ ] URL matches the API endpoint exactly (correct base URL, path segments, path params)
- [ ] HTTP method matches the API spec (GET, POST, PUT, PATCH, DELETE)
- [ ] Headers include correct auth pattern:
- OAuth: `Authorization: Bearer ${params.accessToken}`
- API Key: correct header name and format per the service's docs
- [ ] `Content-Type` header is set for POST/PUT/PATCH requests
- [ ] Body sends all required fields and only includes optional fields when provided
- [ ] For GET requests with query params: URL is constructed correctly with query string
- [ ] ID fields in URL paths are `.trim()`-ed to prevent copy-paste whitespace errors
- [ ] Path params use template literals correctly: `` `https://api.service.com/v1/${params.id.trim()}` ``
### Response / transformResponse
- [ ] Correctly parses the API response (`await response.json()`)
- [ ] Extracts the right fields from the response structure (e.g., `data.data` vs `data` vs `data.results`)
- [ ] All nullable fields use `?? null`
- [ ] All optional arrays use `?? []`
- [ ] Error cases are handled: checks for missing/empty data and returns meaningful error
- [ ] Does NOT do raw JSON dumps — extracts meaningful, individual fields
### Outputs
- [ ] All output fields match what the API actually returns
- [ ] No fields are missing that the API provides and users would commonly need
- [ ] No phantom fields defined that the API doesn't return
- [ ] `optional: true` is set on fields that may not exist in all responses
- [ ] When using `type: 'json'` and the shape is known, `properties` defines the inner fields
- [ ] When using `type: 'array'`, `items` defines the item structure with `properties`
- [ ] Field descriptions are accurate and helpful
### Types (types.ts)
- [ ] Has param interfaces for every tool (e.g., `XCreateTweetParams`)
- [ ] Has response interfaces for every tool (extending `ToolResponse`)
- [ ] Optional params use `?` in the interface (e.g., `replyTo?: string`)
- [ ] Field names in types match actual API field names
- [ ] Shared response types are properly reused (e.g., `XTweetResponse` shared across tweet tools)
### Barrel Export (index.ts)
- [ ] Every tool is exported
- [ ] All types are re-exported (`export * from './types'`)
- [ ] No orphaned exports (tools that don't exist)
### Tool Registry (tools/registry.ts)
- [ ] Every tool is imported and registered
- [ ] Registry keys use snake_case and match tool IDs exactly
- [ ] Entries are in alphabetical order within the file
## Step 4: Validate Block
### Block ↔ Tool Alignment (CRITICAL)
This is the most important validation — the block must be perfectly aligned with every tool it references.
For **each tool** in `tools.access`:
- [ ] The operation dropdown has an option whose ID matches the tool ID (or the `tools.config.tool` function correctly maps to it)
- [ ] Every **required** tool param (except `accessToken`) has a corresponding subBlock input that is:
- Shown when that operation is selected (correct `condition`)
- Marked as `required: true` (or conditionally required)
- [ ] Every **optional** tool param has a corresponding subBlock input (or is intentionally omitted if truly never needed)
- [ ] SubBlock `id` values are unique across the entire block — no duplicates even across different conditions
- [ ] The `tools.config.tool` function returns the correct tool ID for every possible operation value
- [ ] The `tools.config.params` function correctly maps subBlock IDs to tool param names when they differ
### SubBlocks
- [ ] Operation dropdown lists ALL tool operations available in `tools.access`
- [ ] Dropdown option labels are human-readable and descriptive
- [ ] Conditions use correct syntax:
- Single value: `{ field: 'operation', value: 'x_create_tweet' }`
- Multiple values (OR): `{ field: 'operation', value: ['x_create_tweet', 'x_delete_tweet'] }`
- Negation: `{ field: 'operation', value: 'delete', not: true }`
- Compound: `{ field: 'op', value: 'send', and: { field: 'type', value: 'dm' } }`
- [ ] Condition arrays include ALL operations that use that field — none missing
- [ ] `dependsOn` is set for fields that need other values (selectors depending on credential, cascading dropdowns)
- [ ] SubBlock types match tool param types:
- Enum/fixed options → `dropdown`
- Free text → `short-input`
- Long text/content → `long-input`
- True/false → `dropdown` with Yes/No options (not `switch` unless purely UI toggle)
- Credentials → `oauth-input` with correct `serviceId`
- [ ] Dropdown `value: () => 'default'` is set for dropdowns with a sensible default
### Advanced Mode
- [ ] Optional, rarely-used fields are set to `mode: 'advanced'`:
- Pagination tokens / next tokens
- Time range filters (start/end time)
- Sort order / direction options
- Max results / per page limits
- Reply settings / threading options
- Rarely used IDs (reply-to, quote-tweet, etc.)
- Exclude filters
- [ ] **Required** fields are NEVER set to `mode: 'advanced'`
- [ ] Fields that users fill in most of the time are NOT set to `mode: 'advanced'`
### WandConfig
- [ ] Timestamp fields have `wandConfig` with `generationType: 'timestamp'`
- [ ] Comma-separated list fields have `wandConfig` with a descriptive prompt
- [ ] Complex filter/query fields have `wandConfig` with format examples in the prompt
- [ ] All `wandConfig` prompts end with "Return ONLY the [format] - no explanations, no extra text."
- [ ] `wandConfig.placeholder` describes what to type in natural language
### Tools Config
- [ ] `tools.access` lists **every** tool ID the block can use — none missing
- [ ] `tools.config.tool` returns the correct tool ID for each operation
- [ ] Type coercions are in `tools.config.params` (runs at execution time), NOT in `tools.config.tool` (runs at serialization time before variable resolution)
- [ ] `tools.config.params` handles:
- `Number()` conversion for numeric params that come as strings from inputs
- `Boolean` / string-to-boolean conversion for toggle params
- Empty string → `undefined` conversion for optional dropdown values
- Any subBlock ID → tool param name remapping
- [ ] No `Number()`, `JSON.parse()`, or other coercions in `tools.config.tool` — these would destroy dynamic references like `<Block.output>`
### Block Outputs
- [ ] Outputs cover the key fields returned by ALL tools (not just one operation)
- [ ] Output types are correct (`'string'`, `'number'`, `'boolean'`, `'json'`)
- [ ] `type: 'json'` outputs either:
- Describe inner fields in the description string (GOOD): `'User profile (id, name, username, bio)'`
- Use nested output definitions (BEST): `{ id: { type: 'string' }, name: { type: 'string' } }`
- [ ] No opaque `type: 'json'` with vague descriptions like `'Response data'`
- [ ] Outputs that only appear for certain operations use `condition` if supported, or document which operations return them
### Block Metadata
- [ ] `type` is snake_case (e.g., `'x'`, `'cloudflare'`)
- [ ] `name` is human-readable (e.g., `'X'`, `'Cloudflare'`)
- [ ] `description` is a concise one-liner
- [ ] `longDescription` provides detail for docs
- [ ] `docsLink` points to `'https://docs.sim.ai/tools/{service}'`
- [ ] `category` is `'tools'`
- [ ] `bgColor` uses the service's brand color hex
- [ ] `icon` references the correct icon component from `@/components/icons`
- [ ] `authMode` is set correctly (`AuthMode.OAuth` or `AuthMode.ApiKey`)
- [ ] Block is registered in `blocks/registry.ts` alphabetically
### Block Inputs
- [ ] `inputs` section lists all subBlock params that the block accepts
- [ ] Input types match the subBlock types
- [ ] When using `canonicalParamId`, inputs list the canonical ID (not the raw subBlock IDs)
## Step 5: Validate OAuth Scopes (if OAuth service)
Scopes are centralized — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`.
- [ ] Scopes defined in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS[provider].services[service].scopes`
- [ ] `auth.ts` uses `getCanonicalScopesForProvider(providerId)` — NOT a hardcoded array
- [ ] Block `requiredScopes` uses `getScopesForService(serviceId)` — NOT a hardcoded array
- [ ] No hardcoded scope arrays in `auth.ts` or block files (should all use utility functions)
- [ ] Each scope has a human-readable description in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
- [ ] No excess scopes that aren't needed by any tool
## Step 6: Validate Pagination Consistency
If any tools support pagination:
- [ ] Pagination param names match the API docs (e.g., `pagination_token` vs `next_token` vs `cursor`)
- [ ] Different API endpoints that use different pagination param names have separate subBlocks in the block
- [ ] Pagination response fields (`nextToken`, `cursor`, etc.) are included in tool outputs
- [ ] Pagination subBlocks are set to `mode: 'advanced'`
## Step 7: Validate Error Handling
- [ ] `transformResponse` checks for error conditions before accessing data
- [ ] Error responses include meaningful messages (not just generic "failed")
- [ ] HTTP error status codes are handled (check `response.ok` or status codes)
## Step 8: Report and Fix
### Report Format
Group findings by severity:
**Critical** (will cause runtime errors or incorrect behavior):
- Wrong endpoint URL or HTTP method
- Missing required params or wrong `required` flag
- Incorrect response field mapping (accessing wrong path in response)
- Missing error handling that would cause crashes
- Tool ID mismatch between tool file, registry, and block `tools.access`
- OAuth scopes missing in `auth.ts` that tools need
- `tools.config.tool` returning wrong tool ID for an operation
- Type coercions in `tools.config.tool` instead of `tools.config.params`
**Warning** (follows conventions incorrectly or has usability issues):
- Optional field not set to `mode: 'advanced'`
- Missing `wandConfig` on timestamp/complex fields
- Wrong `visibility` on params (e.g., `'hidden'` instead of `'user-or-llm'`)
- Missing `optional: true` on nullable outputs
- Opaque `type: 'json'` without property descriptions
- Missing `.trim()` on ID fields in request URLs
- Missing `?? null` on nullable response fields
- Block condition array missing an operation that uses that field
- Hardcoded scope arrays instead of using `getScopesForService()` / `getCanonicalScopesForProvider()`
- Missing scope description in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
**Suggestion** (minor improvements):
- Better description text
- Inconsistent naming across tools
- Missing `longDescription` or `docsLink`
- Pagination fields that could benefit from `wandConfig`
### Fix All Issues
After reporting, fix every **critical** and **warning** issue. Apply **suggestions** where they don't add unnecessary complexity.
### Validation Output
After fixing, confirm:
1. `bun run lint` passes with no fixes needed
2. TypeScript compiles clean (no type errors)
3. Re-read all modified files to verify fixes are correct
## Checklist Summary
- [ ] Read ALL tool files, block, types, index, and registries
- [ ] Pulled and read official API documentation
- [ ] Validated every tool's ID, params, request, response, outputs, and types against API docs
- [ ] Validated block ↔ tool alignment (every tool param has a subBlock, every condition is correct)
- [ ] Validated advanced mode on optional/rarely-used fields
- [ ] Validated wandConfig on timestamps and complex inputs
- [ ] Validated tools.config mapping, tool selector, and type coercions
- [ ] Validated block outputs match what tools return, with typed JSON where possible
- [ ] Validated OAuth scopes use centralized utilities (getScopesForService, getCanonicalScopesForProvider) — no hardcoded arrays
- [ ] Validated scope descriptions exist in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts` for all scopes
- [ ] Validated pagination consistency across tools and block
- [ ] Validated error handling (error checks, meaningful messages)
- [ ] Validated registry entries (tools and block, alphabetical, correct imports)
- [ ] Reported all issues grouped by severity
- [ ] Fixed all critical and warning issues
- [ ] Ran `bun run lint` after fixes
- [ ] Verified TypeScript compiles clean

View File

@@ -1,35 +0,0 @@
---
paths:
- "apps/sim/components/emcn/**"
---
# EMCN Components
Import from `@/components/emcn`, never from subpaths (except CSS files).
## CVA vs Direct Styles
**Use CVA when:** 2+ variants (primary/secondary, sm/md/lg)
```tsx
const buttonVariants = cva('base-classes', {
variants: { variant: { default: '...', primary: '...' } }
})
export { Button, buttonVariants }
```
**Use direct className when:** Single consistent style, no variations
```tsx
function Label({ className, ...props }) {
return <Primitive className={cn('style-classes', className)} {...props} />
}
```
## Rules
- Use Radix UI primitives for accessibility
- Export component and variants (if using CVA)
- TSDoc with usage examples
- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]`
- `transition-colors` for hover states

View File

@@ -1,13 +0,0 @@
# Global Standards
## Logging
Import `createLogger` from `sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
## Comments
Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
## Styling
Never update global styles. Keep all styling local to components.
## Package Manager
Use `bun` and `bunx`, not `npm` and `npx`.

View File

@@ -1,56 +0,0 @@
---
paths:
- "apps/sim/**"
---
# Sim App Architecture
## Core Principles
1. **Single Responsibility**: Each component, hook, store has one clear purpose
2. **Composition Over Complexity**: Break down complex logic into smaller pieces
3. **Type Safety First**: TypeScript interfaces for all props, state, return types
4. **Predictable State**: Zustand for global state, useState for UI-only concerns
## Root-Level Structure
```
apps/sim/
├── app/ # Next.js app router (pages, API routes)
├── blocks/ # Block definitions and registry
├── components/ # Shared UI (emcn/, ui/)
├── executor/ # Workflow execution engine
├── hooks/ # Shared hooks (queries/, selectors/)
├── lib/ # App-wide utilities
├── providers/ # LLM provider integrations
├── stores/ # Zustand stores
├── tools/ # Tool definitions
└── triggers/ # Trigger definitions
```
## Feature Organization
Features live under `app/workspace/[workspaceId]/`:
```
feature/
├── components/ # Feature components
├── hooks/ # Feature-scoped hooks
├── utils/ # Feature-scoped utilities (2+ consumers)
├── feature.tsx # Main component
└── page.tsx # Next.js page entry
```
## Naming Conventions
- **Components**: PascalCase (`WorkflowList`)
- **Hooks**: `use` prefix (`useWorkflowOperations`)
- **Files**: kebab-case (`workflow-list.tsx`)
- **Stores**: `stores/feature/store.ts`
- **Constants**: SCREAMING_SNAKE_CASE
- **Interfaces**: PascalCase with suffix (`WorkflowListProps`)
## Utils Rules
- **Never create `utils.ts` for single consumer** - inline it
- **Create `utils.ts` when** 2+ files need the same helper
- **Check existing sources** before duplicating (`lib/` has many utilities)
- **Location**: `lib/` (app-wide) → `feature/utils/` (feature-scoped) → inline (single-use)

View File

@@ -1,48 +0,0 @@
---
paths:
- "apps/sim/**/*.tsx"
---
# Component Patterns
## Structure Order
```typescript
'use client' // Only if using hooks
// Imports (external → internal)
// Constants at module level
const CONFIG = { SPACING: 8 } as const
// Props interface
interface ComponentProps {
requiredProp: string
optionalProp?: boolean
}
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
// a. Refs
// b. External hooks (useParams, useRouter)
// c. Store hooks
// d. Custom hooks
// e. Local state
// f. useMemo
// g. useCallback
// h. useEffect
// i. Return JSX
}
```
## Rules
1. `'use client'` only when using React hooks
2. Always define props interface
3. Extract constants with `as const`
4. Semantic HTML (`aside`, `nav`, `article`)
5. Optional chain callbacks: `onAction?.(id)`
## Component Extraction
**Extract when:** 50+ lines, used in 2+ files, or has own state/logic
**Keep inline when:** < 10 lines, single use, purely presentational

View File

@@ -1,55 +0,0 @@
---
paths:
- "apps/sim/**/use-*.ts"
- "apps/sim/**/hooks/**/*.ts"
---
# Hook Patterns
## Structure
```typescript
interface UseFeatureProps {
id: string
onSuccess?: (result: Result) => void
}
export function useFeature({ id, onSuccess }: UseFeatureProps) {
// 1. Refs for stable dependencies
const idRef = useRef(id)
const onSuccessRef = useRef(onSuccess)
// 2. State
const [data, setData] = useState<Data | null>(null)
const [isLoading, setIsLoading] = useState(false)
// 3. Sync refs
useEffect(() => {
idRef.current = id
onSuccessRef.current = onSuccess
}, [id, onSuccess])
// 4. Operations (useCallback with empty deps when using refs)
const fetchData = useCallback(async () => {
setIsLoading(true)
try {
const result = await fetch(`/api/${idRef.current}`).then(r => r.json())
setData(result)
onSuccessRef.current?.(result)
} finally {
setIsLoading(false)
}
}, [])
return { data, isLoading, fetchData }
}
```
## Rules
1. Single responsibility per hook
2. Props interface required
3. Refs for stable callback dependencies
4. Wrap returned functions in useCallback
5. Always try/catch async operations
6. Track loading/error states

View File

@@ -1,62 +0,0 @@
---
paths:
- "apps/sim/**/*.ts"
- "apps/sim/**/*.tsx"
---
# Import Patterns
## Absolute Imports
**Always use absolute imports.** Never use relative imports.
```typescript
// ✓ Good
import { useWorkflowStore } from '@/stores/workflows/store'
import { Button } from '@/components/ui/button'
// ✗ Bad
import { useWorkflowStore } from '../../../stores/workflows/store'
```
## Barrel Exports
Use barrel exports (`index.ts`) when a folder has 3+ exports. Import from barrel, not individual files.
```typescript
// ✓ Good
import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/components'
// ✗ Bad
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard'
```
## No Re-exports
Do not re-export from non-barrel files. Import directly from the source.
```typescript
// ✓ Good - import from where it's declared
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
// ✗ Bad - re-exporting in utils.ts then importing from there
import { CORE_TRIGGER_TYPES } from '@/app/workspace/.../utils'
```
## Import Order
1. React/core libraries
2. External libraries
3. UI components (`@/components/emcn`, `@/components/ui`)
4. Utilities (`@/lib/...`)
5. Stores (`@/stores/...`)
6. Feature imports
7. CSS imports
## Type Imports
Use `type` keyword for type-only imports:
```typescript
import type { WorkflowLog } from '@/stores/logs/types'
```

View File

@@ -1,287 +0,0 @@
---
paths:
- "apps/sim/tools/**"
- "apps/sim/blocks/**"
- "apps/sim/triggers/**"
---
# Adding Integrations
## Overview
Adding a new integration typically requires:
1. **Tools** - API operations (`tools/{service}/`)
2. **Block** - UI component (`blocks/blocks/{service}.ts`)
3. **Icon** - SVG icon (`components/icons.tsx`)
4. **Trigger** (optional) - Webhooks/polling (`triggers/{service}/`)
Always look up the service's API docs first.
## 1. Tools (`tools/{service}/`)
```
tools/{service}/
├── index.ts # Export all tools
├── types.ts # Params/response types
├── {action}.ts # Individual tool (e.g., send_message.ts)
└── ...
```
**Tool file structure:**
```typescript
// tools/{service}/{action}.ts
import type { {Service}Params, {Service}Response } from '@/tools/{service}/types'
import type { ToolConfig } from '@/tools/types'
export const {service}{Action}Tool: ToolConfig<{Service}Params, {Service}Response> = {
id: '{service}_{action}',
name: '{Service} {Action}',
description: 'What this tool does',
version: '1.0.0',
oauth: { required: true, provider: '{service}' }, // if OAuth
params: { /* param definitions */ },
request: {
url: '/api/tools/{service}/{action}',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({ ...params }),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) throw new Error(data.error)
return { success: true, output: data.output }
},
outputs: { /* output definitions */ },
}
```
**Register in `tools/registry.ts`:**
```typescript
import { {service}{Action}Tool } from '@/tools/{service}'
// Add to registry object
{service}_{action}: {service}{Action}Tool,
```
## 2. Block (`blocks/blocks/{service}.ts`)
```typescript
import { {Service}Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { {Service}Response } from '@/tools/{service}/types'
export const {Service}Block: BlockConfig<{Service}Response> = {
type: '{service}',
name: '{Service}',
description: 'Short description',
longDescription: 'Detailed description',
category: 'tools',
bgColor: '#hexcolor',
icon: {Service}Icon,
subBlocks: [ /* see SubBlock Properties below */ ],
tools: {
access: ['{service}_{action}', ...],
config: {
tool: (params) => `{service}_${params.operation}`,
params: (params) => ({ ...params }),
},
},
inputs: { /* input definitions */ },
outputs: { /* output definitions */ },
}
```
### SubBlock Properties
```typescript
{
id: 'fieldName', // Unique identifier
title: 'Field Label', // UI label
type: 'short-input', // See SubBlock Types below
placeholder: 'Hint text',
required: true, // See Required below
condition: { ... }, // See Condition below
dependsOn: ['otherField'], // See DependsOn below
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
}
```
**SubBlock Types:** `short-input`, `long-input`, `dropdown`, `code`, `switch`, `slider`, `oauth-input`, `channel-selector`, `user-selector`, `file-upload`, etc.
### `condition` - Show/hide based on another field
```typescript
// Show when operation === 'send'
condition: { field: 'operation', value: 'send' }
// Show when operation is 'send' OR 'read'
condition: { field: 'operation', value: ['send', 'read'] }
// Show when operation !== 'send'
condition: { field: 'operation', value: 'send', not: true }
// Complex: NOT in list AND another condition
condition: {
field: 'operation',
value: ['list_channels', 'list_users'],
not: true,
and: { field: 'destinationType', value: 'dm', not: true }
}
```
### `required` - Field validation
```typescript
// Always required
required: true
// Conditionally required (same syntax as condition)
required: { field: 'operation', value: 'send' }
```
### `dependsOn` - Clear field when dependencies change
```typescript
// Clear when credential changes
dependsOn: ['credential']
// Clear when authMethod changes AND (credential OR botToken) changes
dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
```
### `mode` - When to show field
- `'basic'` - Only in basic mode (default UI)
- `'advanced'` - Only in advanced mode (manual input)
- `'both'` - Show in both modes (default)
- `'trigger'` - Only when block is used as trigger
### `canonicalParamId` - Link basic/advanced alternatives
Use to map multiple UI inputs to a single logical parameter:
```typescript
// Basic mode: Visual selector
{
id: 'fileSelector',
type: 'file-selector',
mode: 'basic',
canonicalParamId: 'fileId',
required: true,
},
// Advanced mode: Manual input
{
id: 'manualFileId',
type: 'short-input',
mode: 'advanced',
canonicalParamId: 'fileId',
required: true,
},
```
**Critical Rules:**
- `canonicalParamId` must NOT match any subblock's `id`
- `canonicalParamId` must be unique per operation/condition context
- **Required consistency:** All subblocks in a canonical group must have the same `required` status
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs
- **Params function:** Must use canonical param IDs (raw IDs are deleted after canonical transformation)
**Register in `blocks/registry.ts`:**
```typescript
import { {Service}Block } from '@/blocks/blocks/{service}'
// Add to registry object (alphabetically)
{service}: {Service}Block,
```
## 3. Icon (`components/icons.tsx`)
```typescript
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 path from service's brand assets */}
</svg>
)
}
```
## 4. Trigger (`triggers/{service}/`) - Optional
```
triggers/{service}/
├── index.ts # Export all triggers
├── webhook.ts # Webhook handler
├── utils.ts # Shared utilities
└── {event}.ts # Specific event handlers
```
**Register in `triggers/registry.ts`:**
```typescript
import { {service}WebhookTrigger } from '@/triggers/{service}'
// Add to TRIGGER_REGISTRY
{service}_webhook: {service}WebhookTrigger,
```
## File Handling
When integrations handle file uploads/downloads, use `UserFile` objects consistently.
### File Input (Uploads)
1. **Block subBlocks:** Use basic/advanced mode pattern with `canonicalParamId`
2. **Normalize in block config:** Use `normalizeFileInput` from `@/blocks/utils`
3. **Internal API route:** Create route that uses `downloadFileFromStorage` to get file content
4. **Tool routes to internal API:** Don't call external APIs directly with files
```typescript
// In block tools.config:
import { normalizeFileInput } from '@/blocks/utils'
const normalizedFile = normalizeFileInput(
params.uploadFile || params.fileRef || params.fileContent,
{ single: true }
)
if (normalizedFile) params.file = normalizedFile
```
### File Output (Downloads)
Use `FileToolProcessor` in tool `transformResponse` to store files:
```typescript
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
const processor = new FileToolProcessor(context)
const file = await processor.processFileData({
data: base64Content,
mimeType: 'application/pdf',
filename: 'doc.pdf',
})
```
### Key Helpers
| 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 Buffer from UserFile |
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
## Checklist
- [ ] Look up API docs for the service
- [ ] Create `tools/{service}/types.ts` with proper types
- [ ] Create tool files for each operation
- [ ] Create `tools/{service}/index.ts` barrel export
- [ ] Register tools in `tools/registry.ts`
- [ ] Add icon to `components/icons.tsx`
- [ ] Create block in `blocks/blocks/{service}.ts`
- [ ] Register block in `blocks/registry.ts`
- [ ] (Optional) Create triggers in `triggers/{service}/`
- [ ] (Optional) Register triggers in `triggers/registry.ts`
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
- [ ] (If file uploads) Use `normalizeFileInput` in block config

View File

@@ -1,66 +0,0 @@
---
paths:
- "apps/sim/hooks/queries/**/*.ts"
---
# React Query Patterns
All React Query hooks live in `hooks/queries/`.
## Query Key Factory
Every query file defines a keys factory:
```typescript
export const entityKeys = {
all: ['entity'] as const,
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] as const,
detail: (id?: string) => [...entityKeys.all, 'detail', id ?? ''] as const,
}
```
## File Structure
```typescript
// 1. Query keys factory
// 2. Types (if needed)
// 3. Private fetch functions
// 4. Exported hooks
```
## Query Hook
```typescript
export function useEntityList(workspaceId?: string, options?: { enabled?: boolean }) {
return useQuery({
queryKey: entityKeys.list(workspaceId),
queryFn: () => fetchEntities(workspaceId as string),
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
staleTime: 60 * 1000,
placeholderData: keepPreviousData,
})
}
```
## Mutation Hook
```typescript
export function useCreateEntity() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (variables) => { /* fetch POST */ },
onSuccess: () => queryClient.invalidateQueries({ queryKey: entityKeys.all }),
})
}
```
## Optimistic Updates
For optimistic mutations syncing with Zustand, use `createOptimisticMutationHandlers` from `@/hooks/queries/utils/optimistic-mutation`.
## Naming
- **Keys**: `entityKeys`
- **Query hooks**: `useEntity`, `useEntityList`
- **Mutation hooks**: `useCreateEntity`, `useUpdateEntity`
- **Fetch functions**: `fetchEntity` (private)

View File

@@ -1,71 +0,0 @@
---
paths:
- "apps/sim/**/store.ts"
- "apps/sim/**/stores/**/*.ts"
---
# Zustand Store Patterns
Stores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.
## Basic Store
```typescript
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import type { FeatureState } from '@/stores/feature/types'
const initialState = { items: [] as Item[], activeId: null as string | null }
export const useFeatureStore = create<FeatureState>()(
devtools(
(set, get) => ({
...initialState,
setItems: (items) => set({ items }),
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
reset: () => set(initialState),
}),
{ name: 'feature-store' }
)
)
```
## Persisted Store
```typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useFeatureStore = create<FeatureState>()(
persist(
(set) => ({
width: 300,
setWidth: (width) => set({ width }),
_hasHydrated: false,
setHasHydrated: (v) => set({ _hasHydrated: v }),
}),
{
name: 'feature-state',
partialize: (state) => ({ width: state.width }),
onRehydrateStorage: () => (state) => state?.setHasHydrated(true),
}
)
)
```
## Rules
1. Use `devtools` middleware (named stores)
2. Use `persist` only when data should survive reload
3. `partialize` to persist only necessary state
4. `_hasHydrated` pattern for persisted stores needing hydration tracking
5. Immutable updates only
6. `set((state) => ...)` when depending on previous state
7. Provide `reset()` action
## Outside React
```typescript
const items = useFeatureStore.getState().items
useFeatureStore.setState({ items: newItems })
```

View File

@@ -1,41 +0,0 @@
---
paths:
- "apps/sim/**/*.tsx"
- "apps/sim/**/*.css"
---
# Styling Rules
## Tailwind
1. **No inline styles** - Use Tailwind classes
2. **No duplicate dark classes** - Skip `dark:` when value matches light mode
3. **Exact values** - `text-[14px]`, `h-[26px]`
4. **Transitions** - `transition-colors` for interactive states
## Conditional Classes
```typescript
import { cn } from '@/lib/utils'
<div className={cn(
'base-classes',
isActive && 'active-classes',
disabled ? 'opacity-60' : 'hover:bg-accent'
)} />
```
## CSS Variables
For dynamic values (widths, heights) synced with stores:
```typescript
// In store
setWidth: (width) => {
set({ width })
document.documentElement.style.setProperty('--sidebar-width', `${width}px`)
}
// In component
<aside style={{ width: 'var(--sidebar-width)' }} />
```

View File

@@ -1,217 +0,0 @@
---
paths:
- "apps/sim/**/*.test.ts"
- "apps/sim/**/*.test.tsx"
---
# Testing Patterns
Use Vitest. Test files: `feature.ts``feature.test.ts`
## Global Mocks (vitest.setup.ts)
These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior:
- `@sim/db``databaseMock`
- `drizzle-orm``drizzleOrmMock`
- `@sim/logger``loggerMock`
- `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store`
- `@/blocks/registry`
- `@trigger.dev/sdk`
## Structure
```typescript
/**
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({
auth: { api: { getSession: vi.fn() } },
getSession: mockGetSession,
}))
import { GET, POST } from '@/app/api/my-route/route'
describe('my route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
})
it('returns data', async () => {
const req = createMockRequest('GET')
const res = await GET(req)
expect(res.status).toBe(200)
})
})
```
## Performance Rules (Critical)
### NEVER use `vi.resetModules()` + `vi.doMock()` + `await import()`
This is the #1 cause of slow tests. It forces complete module re-evaluation per test.
```typescript
// BAD — forces module re-evaluation every test (~50-100ms each)
beforeEach(() => {
vi.resetModules()
vi.doMock('@/lib/auth', () => ({ getSession: vi.fn() }))
})
it('test', async () => {
const { GET } = await import('./route') // slow dynamic import
})
// GOOD — module loaded once, mocks reconfigured per test (~1ms each)
const { mockGetSession } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
import { GET } from '@/app/api/my-route/route'
beforeEach(() => { vi.clearAllMocks() })
it('test', () => {
mockGetSession.mockResolvedValue({ user: { id: '1' } })
})
```
**Only exception:** Singleton modules that cache state at module scope (e.g., Redis clients, connection pools). These genuinely need `vi.resetModules()` + dynamic import to get a fresh instance per test.
### NEVER use `vi.importActual()`
This defeats the purpose of mocking by loading the real module and all its dependencies.
```typescript
// BAD — loads real module + all transitive deps
vi.mock('@/lib/workspaces/utils', async () => {
const actual = await vi.importActual('@/lib/workspaces/utils')
return { ...actual, myFn: vi.fn() }
})
// GOOD — mock everything, only implement what tests need
vi.mock('@/lib/workspaces/utils', () => ({
myFn: vi.fn(),
otherFn: vi.fn(),
}))
```
### NEVER use `mockAuth()`, `mockConsoleLogger()`, or `setupCommonApiMocks()` from `@sim/testing`
These helpers internally use `vi.doMock()` which is slow. Use direct `vi.hoisted()` + `vi.mock()` instead.
### Mock heavy transitive dependencies
If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them:
```typescript
vi.mock('@/blocks', () => ({
getBlock: () => null,
getAllBlocks: () => ({}),
getAllBlockTypes: () => [],
registry: {},
}))
```
### Use `@vitest-environment node` unless DOM is needed
Only use `@vitest-environment jsdom` if the test uses `window`, `document`, `FormData`, or other browser APIs. Node environment is significantly faster.
### Avoid real timers in tests
```typescript
// BAD
await new Promise(r => setTimeout(r, 500))
// GOOD — use minimal delays or fake timers
await new Promise(r => setTimeout(r, 1))
// or
vi.useFakeTimers()
```
## Mock Pattern Reference
### Auth mocking (API routes)
```typescript
const { mockGetSession } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({
auth: { api: { getSession: vi.fn() } },
getSession: mockGetSession,
}))
// In tests:
mockGetSession.mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com' } })
mockGetSession.mockResolvedValue(null) // unauthenticated
```
### Hybrid auth mocking
```typescript
const { mockCheckSessionOrInternalAuth } = vi.hoisted(() => ({
mockCheckSessionOrInternalAuth: vi.fn(),
}))
vi.mock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
}))
// In tests:
mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true, userId: 'user-1', authType: 'session',
})
```
### Database chain mocking
```typescript
const { mockSelect, mockFrom, mockWhere } = vi.hoisted(() => ({
mockSelect: vi.fn(),
mockFrom: vi.fn(),
mockWhere: vi.fn(),
}))
vi.mock('@sim/db', () => ({
db: { select: mockSelect },
}))
beforeEach(() => {
mockSelect.mockReturnValue({ from: mockFrom })
mockFrom.mockReturnValue({ where: mockWhere })
mockWhere.mockResolvedValue([{ id: '1', name: 'test' }])
})
```
## @sim/testing Package
Always prefer over local test data.
| Category | Utilities |
|----------|-----------|
| **Mocks** | `loggerMock`, `databaseMock`, `drizzleOrmMock`, `setupGlobalFetchMock()` |
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
| **Requests** | `createMockRequest()`, `createEnvMock()` |
## Rules Summary
1. `@vitest-environment node` unless DOM is required
2. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
3. `vi.mock()` calls before importing mocked modules
4. `@sim/testing` utilities over local mocks
5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach`
6. No `vi.importActual()` — mock everything explicitly
7. No `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` — use direct mocks
8. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
9. Use absolute imports in test files
10. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`

View File

@@ -1,21 +0,0 @@
---
paths:
- "apps/sim/**/*.ts"
- "apps/sim/**/*.tsx"
---
# TypeScript Rules
1. **No `any`** - Use proper types or `unknown` with type guards
2. **Props interface** - Always define for components
3. **Const assertions** - `as const` for constant objects/arrays
4. **Ref types** - Explicit: `useRef<HTMLDivElement>(null)`
5. **Type imports** - `import type { X }` for type-only imports
```typescript
// ✗ Bad
const handleClick = (e: any) => {}
// ✓ Good
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {}
```

View File

@@ -1,7 +0,0 @@
Based on the given area of interest, please:
1. Dig around the codebase in terms of that given area of interest, gather general information such as keywords and architecture overview.
2. Spawn off n=10 (unless specified otherwise) task agents to dig deeper into the codebase in terms of that given area of interest, some of them should be out of the box for variance.
3. Once the task agents are done, use the information to do what the user wants.
If user is in plan mode, use the information to create the plan.

View File

@@ -8,7 +8,7 @@ alwaysApply: true
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
## Logging
Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
Import `createLogger` from `sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
## Comments
Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.

View File

@@ -1,26 +0,0 @@
---
description: SEO and GEO guidelines for the landing page
globs: ["apps/sim/app/(home)/**/*.tsx"]
---
# Landing Page — SEO / GEO
## SEO
- One `<h1>` per page, in Hero only — never add another.
- Strict heading hierarchy: H1 (Hero) → H2 (section titles) → H3 (feature names).
- Every section: `<section id="…" aria-labelledby="…-heading">`.
- Decorative/animated elements: `aria-hidden="true"`.
- All internal routes use Next.js `<Link>` (crawlable). External links get `rel="noopener noreferrer"`.
- Navbar is a Server Component (no `'use client'`) for immediate crawlability. Logo `<Image>` has `priority` (LCP element).
- Navbar `<nav>` carries `SiteNavigationElement` schema.org markup.
- Feature lists must stay in sync with `WebApplication.featureList` in `structured-data.tsx`.
## GEO (Generative Engine Optimisation)
- **Answer-first pattern**: each section's H2 + subtitle should directly answer a user question (e.g. "What is Sim?", "How fast can I deploy?").
- **Atomic answer blocks**: each feature / template card should be independently extractable by an AI summariser.
- **Entity consistency**: always write "Sim" by name — never "the platform" or "our tool".
- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI agents", "agentic workflows".
- **sr-only summaries**: Hero and Templates each have a `<p className="sr-only">` (~50 words) as an atomic product/catalog summary for AI citation.
- **Specific numbers**: prefer concrete figures ("1,000+ integrations", "15+ AI providers") over vague claims.

View File

@@ -155,36 +155,6 @@ dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
- `'both'` - Show in both modes (default)
- `'trigger'` - Only when block is used as trigger
### `canonicalParamId` - Link basic/advanced alternatives
Use to map multiple UI inputs to a single logical parameter:
```typescript
// Basic mode: Visual selector
{
id: 'fileSelector',
type: 'file-selector',
mode: 'basic',
canonicalParamId: 'fileId',
required: true,
},
// Advanced mode: Manual input
{
id: 'manualFileId',
type: 'short-input',
mode: 'advanced',
canonicalParamId: 'fileId',
required: true,
},
```
**Critical Rules:**
- `canonicalParamId` must NOT match any subblock's `id`
- `canonicalParamId` must be unique per operation/condition context
- **Required consistency:** All subblocks in a canonical group must have the same `required` status
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs
- **Params function:** Must use canonical param IDs (raw IDs are deleted after canonical transformation)
**Register in `blocks/registry.ts`:**
```typescript
@@ -223,52 +193,6 @@ import { {service}WebhookTrigger } from '@/triggers/{service}'
{service}_webhook: {service}WebhookTrigger,
```
## File Handling
When integrations handle file uploads/downloads, use `UserFile` objects consistently.
### File Input (Uploads)
1. **Block subBlocks:** Use basic/advanced mode pattern with `canonicalParamId`
2. **Normalize in block config:** Use `normalizeFileInput` from `@/blocks/utils`
3. **Internal API route:** Create route that uses `downloadFileFromStorage` to get file content
4. **Tool routes to internal API:** Don't call external APIs directly with files
```typescript
// In block tools.config:
import { normalizeFileInput } from '@/blocks/utils'
const normalizedFile = normalizeFileInput(
params.uploadFile || params.fileRef || params.fileContent,
{ single: true }
)
if (normalizedFile) params.file = normalizedFile
```
### File Output (Downloads)
Use `FileToolProcessor` in tool `transformResponse` to store files:
```typescript
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
const processor = new FileToolProcessor(context)
const file = await processor.processFileData({
data: base64Content,
mimeType: 'application/pdf',
filename: 'doc.pdf',
})
```
### Key Helpers
| 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 Buffer from UserFile |
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
## Checklist
- [ ] Look up API docs for the service
@@ -281,5 +205,3 @@ const file = await processor.processFileData({
- [ ] Register block in `blocks/registry.ts`
- [ ] (Optional) Create triggers in `triggers/{service}/`
- [ ] (Optional) Register triggers in `triggers/registry.ts`
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
- [ ] (If file uploads) Use `normalizeFileInput` in block config

View File

@@ -5,122 +5,62 @@ globs: ["apps/sim/hooks/queries/**/*.ts"]
# React Query Patterns
All React Query hooks live in `hooks/queries/`. All server state must go through React Query — never use `useState` + `fetch` in components for data fetching or mutations.
All React Query hooks live in `hooks/queries/`.
## Query Key Factory
Every query file defines a hierarchical keys factory with an `all` root key and intermediate plural keys for prefix-level invalidation:
Every query file defines a keys factory:
```typescript
export const entityKeys = {
all: ['entity'] as const,
lists: () => [...entityKeys.all, 'list'] as const,
list: (workspaceId?: string) => [...entityKeys.lists(), workspaceId ?? ''] as const,
details: () => [...entityKeys.all, 'detail'] as const,
detail: (id?: string) => [...entityKeys.details(), id ?? ''] as const,
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] as const,
detail: (id?: string) => [...entityKeys.all, 'detail', id ?? ''] as const,
}
```
Never use inline query keys — always use the factory.
## File Structure
```typescript
// 1. Query keys factory
// 2. Types (if needed)
// 3. Private fetch functions (accept signal parameter)
// 3. Private fetch functions
// 4. Exported hooks
```
## Query Hook
- Every `queryFn` must destructure and forward `signal` for request cancellation
- Every query must have an explicit `staleTime`
- Use `keepPreviousData` only on variable-key queries (where params change), never on static keys
```typescript
async function fetchEntities(workspaceId: string, signal?: AbortSignal) {
const response = await fetch(`/api/entities?workspaceId=${workspaceId}`, { signal })
if (!response.ok) throw new Error('Failed to fetch entities')
return response.json()
}
export function useEntityList(workspaceId?: string, options?: { enabled?: boolean }) {
return useQuery({
queryKey: entityKeys.list(workspaceId),
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
queryFn: () => fetchEntities(workspaceId as string),
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
staleTime: 60 * 1000,
placeholderData: keepPreviousData, // OK: workspaceId varies
placeholderData: keepPreviousData,
})
}
```
## Mutation Hook
- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible
- Invalidation must cover all affected query key prefixes (lists, details, related views)
```typescript
export function useCreateEntity() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (variables) => { /* fetch POST */ },
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: entityKeys.all }),
})
}
```
## Optimistic Updates
For optimistic mutations, use `onSettled` (not `onSuccess`) for cache reconciliation — `onSettled` fires on both success and error, ensuring the cache is always reconciled with the server.
```typescript
export function useUpdateEntity() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (variables) => { /* ... */ },
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey: entityKeys.detail(variables.id) })
const previous = queryClient.getQueryData(entityKeys.detail(variables.id))
queryClient.setQueryData(entityKeys.detail(variables.id), /* optimistic value */)
return { previous }
},
onError: (_err, variables, context) => {
queryClient.setQueryData(entityKeys.detail(variables.id), context?.previous)
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
queryClient.invalidateQueries({ queryKey: entityKeys.detail(variables.id) })
},
})
}
```
For optimistic mutations syncing with Zustand, use `createOptimisticMutationHandlers` from `@/hooks/queries/utils/optimistic-mutation`.
## useCallback Dependencies
Never include mutation objects (e.g., `createEntity`) in `useCallback` dependency arrays — the mutation object is not referentially stable and changes on every state update. The `.mutate()` and `.mutateAsync()` functions are stable in TanStack Query v5.
```typescript
// ✗ Bad — causes unnecessary recreations
const handler = useCallback(() => {
createEntity.mutate(data)
}, [createEntity]) // unstable reference
// ✓ Good — omit from deps, mutate is stable
const handler = useCallback(() => {
createEntity.mutate(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])
```
## Naming
- **Keys**: `entityKeys`
- **Query hooks**: `useEntity`, `useEntityList`
- **Mutation hooks**: `useCreateEntity`, `useUpdateEntity`, `useDeleteEntity`
- **Fetch functions**: `fetchEntity`, `fetchEntities` (private)
- **Mutation hooks**: `useCreateEntity`, `useUpdateEntity`
- **Fetch functions**: `fetchEntity` (private)

View File

@@ -7,210 +7,51 @@ globs: ["apps/sim/**/*.test.ts", "apps/sim/**/*.test.tsx"]
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
## Global Mocks (vitest.setup.ts)
These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior:
- `@sim/db` → `databaseMock`
- `drizzle-orm` → `drizzleOrmMock`
- `@sim/logger` → `loggerMock`
- `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store`
- `@/blocks/registry`
- `@trigger.dev/sdk`
## Structure
```typescript
/**
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { databaseMock, loggerMock } from '@sim/testing'
import { describe, expect, it, vi } from 'vitest'
const { mockGetSession } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
}))
vi.mock('@sim/db', () => databaseMock)
vi.mock('@sim/logger', () => loggerMock)
vi.mock('@/lib/auth', () => ({
auth: { api: { getSession: vi.fn() } },
getSession: mockGetSession,
}))
import { myFunction } from '@/lib/feature'
import { GET, POST } from '@/app/api/my-route/route'
describe('my route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
})
it('returns data', async () => {
const req = createMockRequest('GET')
const res = await GET(req)
expect(res.status).toBe(200)
})
})
```
## Performance Rules (Critical)
### NEVER use `vi.resetModules()` + `vi.doMock()` + `await import()`
This is the #1 cause of slow tests. It forces complete module re-evaluation per test.
```typescript
// BAD — forces module re-evaluation every test (~50-100ms each)
beforeEach(() => {
vi.resetModules()
vi.doMock('@/lib/auth', () => ({ getSession: vi.fn() }))
})
it('test', async () => {
const { GET } = await import('./route') // slow dynamic import
})
// GOOD — module loaded once, mocks reconfigured per test (~1ms each)
const { mockGetSession } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
import { GET } from '@/app/api/my-route/route'
beforeEach(() => { vi.clearAllMocks() })
it('test', () => {
mockGetSession.mockResolvedValue({ user: { id: '1' } })
})
```
**Only exception:** Singleton modules that cache state at module scope (e.g., Redis clients, connection pools). These genuinely need `vi.resetModules()` + dynamic import to get a fresh instance per test.
### NEVER use `vi.importActual()`
This defeats the purpose of mocking by loading the real module and all its dependencies.
```typescript
// BAD — loads real module + all transitive deps
vi.mock('@/lib/workspaces/utils', async () => {
const actual = await vi.importActual('@/lib/workspaces/utils')
return { ...actual, myFn: vi.fn() }
})
// GOOD — mock everything, only implement what tests need
vi.mock('@/lib/workspaces/utils', () => ({
myFn: vi.fn(),
otherFn: vi.fn(),
}))
```
### NEVER use `mockAuth()`, `mockConsoleLogger()`, or `setupCommonApiMocks()` from `@sim/testing`
These helpers internally use `vi.doMock()` which is slow. Use direct `vi.hoisted()` + `vi.mock()` instead.
### Mock heavy transitive dependencies
If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them:
```typescript
vi.mock('@/blocks', () => ({
getBlock: () => null,
getAllBlocks: () => ({}),
getAllBlockTypes: () => [],
registry: {},
}))
```
### Use `@vitest-environment node` unless DOM is needed
Only use `@vitest-environment jsdom` if the test uses `window`, `document`, `FormData`, or other browser APIs. Node environment is significantly faster.
### Avoid real timers in tests
```typescript
// BAD
await new Promise(r => setTimeout(r, 500))
// GOOD — use minimal delays or fake timers
await new Promise(r => setTimeout(r, 1))
// or
vi.useFakeTimers()
```
## Mock Pattern Reference
### Auth mocking (API routes)
```typescript
const { mockGetSession } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({
auth: { api: { getSession: vi.fn() } },
getSession: mockGetSession,
}))
// In tests:
mockGetSession.mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com' } })
mockGetSession.mockResolvedValue(null) // unauthenticated
```
### Hybrid auth mocking
```typescript
const { mockCheckSessionOrInternalAuth } = vi.hoisted(() => ({
mockCheckSessionOrInternalAuth: vi.fn(),
}))
vi.mock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
}))
// In tests:
mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true, userId: 'user-1', authType: 'session',
})
```
### Database chain mocking
```typescript
const { mockSelect, mockFrom, mockWhere } = vi.hoisted(() => ({
mockSelect: vi.fn(),
mockFrom: vi.fn(),
mockWhere: vi.fn(),
}))
vi.mock('@sim/db', () => ({
db: { select: mockSelect },
}))
beforeEach(() => {
mockSelect.mockReturnValue({ from: mockFrom })
mockFrom.mockReturnValue({ where: mockWhere })
mockWhere.mockResolvedValue([{ id: '1', name: 'test' }])
describe('myFunction', () => {
beforeEach(() => vi.clearAllMocks())
it.concurrent('isolated tests run in parallel', () => { ... })
})
```
## @sim/testing Package
Always prefer over local test data.
Always prefer over local mocks.
| Category | Utilities |
|----------|-----------|
| **Mocks** | `loggerMock`, `databaseMock`, `drizzleOrmMock`, `setupGlobalFetchMock()` |
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
| **Mocks** | `loggerMock`, `databaseMock`, `setupGlobalFetchMock()` |
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutorContext()` |
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
| **Requests** | `createMockRequest()`, `createEnvMock()` |
## Rules Summary
## Rules
1. `@vitest-environment node` unless DOM is required
2. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
3. `vi.mock()` calls before importing mocked modules
4. `@sim/testing` utilities over local mocks
5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach`
6. No `vi.importActual()` — mock everything explicitly
7. No `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` — use direct mocks
8. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
9. Use absolute imports in test files
10. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`
1. `@vitest-environment node` directive at file top
2. `vi.mock()` calls before importing mocked modules
3. `@sim/testing` utilities over local mocks
4. `it.concurrent` for isolated tests (no shared mutable state)
5. `beforeEach(() => vi.clearAllMocks())` to reset state
## Hoisted Mocks
For mutable mock references:
```typescript
const mockFn = vi.hoisted(() => vi.fn())
vi.mock('@/lib/module', () => ({ myFunction: mockFn }))
mockFn.mockResolvedValue({ data: 'test' })
```

View File

@@ -1,296 +0,0 @@
---
name: add-hosted-key
description: Add hosted API key support to a tool so Sim provides the key when users don't bring their own. Use when adding hosted keys, BYOK support, hideWhenHosted, or hosted key pricing to a tool or block.
---
# Adding Hosted Key Support to a Tool
When a tool has hosted key support, Sim provides its own API key if the user hasn't configured one (via BYOK or env var). Usage is metered and billed to the workspace.
## Overview
| Step | What | Where |
|------|------|-------|
| 1 | Register BYOK provider ID | `tools/types.ts`, `app/api/workspaces/[id]/byok-keys/route.ts` |
| 2 | Research the API's pricing and rate limits | API docs / pricing page (before writing any code) |
| 3 | Add `hosting` config to the tool | `tools/{service}/{action}.ts` |
| 4 | Hide API key field when hosted | `blocks/blocks/{service}.ts` |
| 5 | Add to BYOK settings UI | BYOK settings component (`byok.tsx`) |
| 6 | Summarize pricing and throttling comparison | Output to user (after all code changes) |
## Step 1: Register the BYOK Provider ID
Add the new provider to the `BYOKProviderId` union in `tools/types.ts`:
```typescript
export type BYOKProviderId =
| 'openai'
| 'anthropic'
// ...existing providers
| 'your_service'
```
Then add it to `VALID_PROVIDERS` in `app/api/workspaces/[id]/byok-keys/route.ts`:
```typescript
const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'your_service'] as const
```
## Step 2: Research the API's Pricing Model and Rate Limits
**Before writing any `getCost` or `rateLimit` code**, look up the service's official documentation for both pricing and rate limits. You need to understand:
### Pricing
1. **How the API charges** — per request, per credit, per token, per step, per minute, etc.
2. **Whether the API reports cost in its response** — look for fields like `creditsUsed`, `costDollars`, `tokensUsed`, or similar in the response body or headers
3. **Whether cost varies by endpoint/options** — some APIs charge more for certain features (e.g., Firecrawl charges 1 credit/page base but +4 for JSON format, +4 for enhanced mode)
4. **The dollar-per-unit rate** — what each credit/token/unit costs in dollars on our plan
### Rate Limits
1. **What rate limits the API enforces** — requests per minute/second, tokens per minute, concurrent requests, etc.
2. **Whether limits vary by plan tier** — free vs paid vs enterprise often have different ceilings
3. **Whether limits are per-key or per-account** — determines whether adding more hosted keys actually increases total throughput
4. **What the API returns when rate limited** — HTTP 429, `Retry-After` header, error body format, etc.
5. **Whether there are multiple dimensions** — some APIs limit both requests/min AND tokens/min independently
Search the API's docs/pricing page (use WebSearch/WebFetch). Capture the pricing model as a comment in `getCost` so future maintainers know the source of truth.
### Setting Our Rate Limits
Our rate limiter (`lib/core/rate-limiter/hosted-key/`) uses a token-bucket algorithm applied **per billing actor** (workspace). It supports two modes:
- **`per_request`** — simple; just `requestsPerMinute`. Good when the API charges flat per-request or cost doesn't vary much.
- **`custom`** — `requestsPerMinute` plus additional `dimensions` (e.g., `tokens`, `search_units`). Each dimension has its own `limitPerMinute` and an `extractUsage` function that reads actual usage from the response. Use when the API charges on a variable metric (tokens, credits) and you want to cap that metric too.
When choosing values for `requestsPerMinute` and any dimension limits:
- **Stay well below the API's per-key limit** — our keys are shared across all workspaces. If the API allows 60 RPM per key and we have 3 keys, the global ceiling is ~180 RPM. Set the per-workspace limit low enough (e.g., 20-60 RPM) that many workspaces can coexist without collectively hitting the API's ceiling.
- **Account for key pooling** — our round-robin distributes requests across `N` hosted keys, so the effective API-side rate per key is `(total requests) / N`. But per-workspace limits are enforced *before* key selection, so they apply regardless of key count.
- **Prefer conservative defaults** — it's easy to raise limits later but hard to claw back after users depend on high throughput.
## Step 3: Add `hosting` Config to the Tool
Add a `hosting` object to the tool's `ToolConfig`. This tells the execution layer how to acquire hosted keys, calculate cost, and rate-limit.
```typescript
hosting: {
envKeyPrefix: 'YOUR_SERVICE_API_KEY',
apiKeyParam: 'apiKey',
byokProviderId: 'your_service',
pricing: {
type: 'custom',
getCost: (_params, output) => {
if (output.creditsUsed == null) {
throw new Error('Response missing creditsUsed field')
}
const creditsUsed = output.creditsUsed as number
const cost = creditsUsed * 0.001 // dollars per credit
return { cost, metadata: { creditsUsed } }
},
},
rateLimit: {
mode: 'per_request',
requestsPerMinute: 100,
},
},
```
### Hosted Key Env Var Convention
Keys use a numbered naming pattern driven by a count env var:
```
YOUR_SERVICE_API_KEY_COUNT=3
YOUR_SERVICE_API_KEY_1=sk-...
YOUR_SERVICE_API_KEY_2=sk-...
YOUR_SERVICE_API_KEY_3=sk-...
```
The `envKeyPrefix` value (`YOUR_SERVICE_API_KEY`) determines which env vars are read at runtime. Adding more keys only requires bumping the count and adding the new env var.
### Pricing: Prefer API-Reported Cost
Always prefer using cost data returned by the API (e.g., `creditsUsed`, `costDollars`). This is the most accurate because it accounts for variable pricing tiers, feature modifiers, and plan-level discounts.
**When the API reports cost** — use it directly and throw if missing:
```typescript
pricing: {
type: 'custom',
getCost: (params, output) => {
if (output.creditsUsed == null) {
throw new Error('Response missing creditsUsed field')
}
// $0.001 per credit — from https://example.com/pricing
const cost = (output.creditsUsed as number) * 0.001
return { cost, metadata: { creditsUsed: output.creditsUsed } }
},
},
```
**When the API does NOT report cost** — compute it from params/output based on the pricing docs, but still validate the data you depend on:
```typescript
pricing: {
type: 'custom',
getCost: (params, output) => {
if (!Array.isArray(output.searchResults)) {
throw new Error('Response missing searchResults, cannot determine cost')
}
// Serper: 1 credit for <=10 results, 2 credits for >10 — from https://serper.dev/pricing
const credits = Number(params.num) > 10 ? 2 : 1
return { cost: credits * 0.001, metadata: { credits } }
},
},
```
**`getCost` must always throw** if it cannot determine cost. Never silently fall back to a default — this would hide billing inaccuracies.
### Capturing Cost Data from the API
If the API returns cost info, capture it in `transformResponse` so `getCost` can read it from the output:
```typescript
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
results: data.results,
creditsUsed: data.creditsUsed, // pass through for getCost
},
}
},
```
For async/polling tools, capture it in `postProcess` when the job completes:
```typescript
if (jobData.status === 'completed') {
result.output = {
data: jobData.data,
creditsUsed: jobData.creditsUsed,
}
}
```
## Step 4: Hide the API Key Field When Hosted
In the block config (`blocks/blocks/{service}.ts`), add `hideWhenHosted: true` to the API key subblock. This hides the field on hosted Sim since the platform provides the key:
```typescript
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
required: true,
hideWhenHosted: true,
},
```
The visibility is controlled by `isSubBlockHiddenByHostedKey()` in `lib/workflows/subblocks/visibility.ts`, which checks the `isHosted` feature flag.
### Excluding Specific Operations from Hosted Key Support
When a block has multiple operations but some operations should **not** use a hosted key (e.g., the underlying API is deprecated, unsupported, or too expensive), use the **duplicate apiKey subblock** pattern. This is the same pattern Exa uses for its `research` operation:
1. **Remove the `hosting` config** from the tool definition for that operation — it must not have a `hosting` object at all.
2. **Duplicate the `apiKey` subblock** in the block config with opposing conditions:
```typescript
// API Key — hidden when hosted for operations with hosted key support
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
required: true,
hideWhenHosted: true,
condition: { field: 'operation', value: 'unsupported_op', not: true },
},
// API Key — always visible for unsupported_op (no hosted key support)
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your API key',
password: true,
required: true,
condition: { field: 'operation', value: 'unsupported_op' },
},
```
Both subblocks share the same `id: 'apiKey'`, so the same value flows to the tool. The conditions ensure only one is visible at a time. The first has `hideWhenHosted: true` and shows for all hosted operations; the second has no `hideWhenHosted` and shows only for the excluded operation — meaning users must always provide their own key for that operation.
To exclude multiple operations, use an array: `{ field: 'operation', value: ['op_a', 'op_b'] }`.
**Reference implementations:**
- **Exa** (`blocks/blocks/exa.ts`): `research` operation excluded from hosting — lines 309-329
- **Google Maps** (`blocks/blocks/google_maps.ts`): `speed_limits` operation excluded from hosting (deprecated Roads API)
## Step 5: Add to the BYOK Settings UI
Add an entry to the `PROVIDERS` array in the BYOK settings component so users can bring their own key. You need the service icon from `components/icons.tsx`:
```typescript
{
id: 'your_service',
name: 'Your Service',
icon: YourServiceIcon,
description: 'What this service does',
placeholder: 'Enter your API key',
},
```
## Step 6: Summarize Pricing and Throttling Comparison
After all code changes are complete, output a detailed summary to the user covering:
### What to include
1. **API's pricing model** — how the service charges (per token, per credit, per request, etc.), the specific rates found in docs, and whether the API reports cost in responses.
2. **Our `getCost` approach** — how we calculate cost, what fields we depend on, and any assumptions or estimates (especially when the API doesn't report exact dollar cost).
3. **API's rate limits** — the documented limits (RPM, TPM, concurrent, etc.), which plan tier they apply to, and whether they're per-key or per-account.
4. **Our `rateLimit` config** — what we set for `requestsPerMinute` (and dimensions if custom mode), why we chose those values, and how they compare to the API's limits.
5. **Key pooling impact** — how many hosted keys we expect, and how round-robin distribution affects the effective per-key rate at the API.
6. **Gaps or risks** — anything the API charges for that we don't meter, rate limit dimensions we chose not to enforce, or pricing that may be inaccurate due to variable model/tier costs.
### Format
Present this as a structured summary with clear headings. Example:
```
### Pricing
- **API charges**: $X per 1M tokens (input), $Y per 1M tokens (output) — varies by model
- **Response reports cost?**: No — only token counts in `usage` field
- **Our getCost**: Estimates cost at $Z per 1M total tokens based on median model pricing
- **Risk**: Actual cost varies by model; our estimate may over/undercharge for cheap/expensive models
### Throttling
- **API limits**: 300 RPM per key (paid tier), 60 RPM (free tier)
- **Per-key or per-account**: Per key — more keys = more throughput
- **Our config**: 60 RPM per workspace (per_request mode)
- **With N keys**: Effective per-key rate is (total RPM across workspaces) / N
- **Headroom**: Comfortable — even 10 active workspaces at full rate = 600 RPM / 3 keys = 200 RPM per key, under the 300 RPM API limit
```
This summary helps reviewers verify that the pricing and rate limiting are well-calibrated and surfaces any risks that need monitoring.
## Checklist
- [ ] Provider added to `BYOKProviderId` in `tools/types.ts`
- [ ] Provider added to `VALID_PROVIDERS` in the BYOK keys API route
- [ ] API pricing docs researched — understand per-unit cost and whether the API reports cost in responses
- [ ] API rate limits researched — understand RPM/TPM limits, per-key vs per-account, and plan tiers
- [ ] `hosting` config added to the tool with `envKeyPrefix`, `apiKeyParam`, `byokProviderId`, `pricing`, and `rateLimit`
- [ ] `getCost` throws if required cost data is missing from the response
- [ ] Cost data captured in `transformResponse` or `postProcess` if API provides it
- [ ] `hideWhenHosted: true` added to the API key subblock in the block config
- [ ] Provider entry added to the BYOK settings UI with icon and description
- [ ] Env vars documented: `{PREFIX}_COUNT` and `{PREFIX}_1..N`
- [ ] Pricing and throttling summary provided to reviewer

View File

@@ -1,4 +1,4 @@
FROM oven/bun:1.3.10-alpine
FROM oven/bun:1.3.3-alpine
# Install necessary packages for development
RUN apk add --no-cache \

View File

@@ -44,7 +44,7 @@ services:
deploy:
resources:
limits:
memory: 1G
memory: 4G
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio

View File

@@ -8,10 +8,7 @@ on:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
cancel-in-progress: false
jobs:
test-build:
@@ -30,11 +27,10 @@ jobs:
steps:
- name: Extract version from commit message
id: extract
env:
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: |
COMMIT_MSG="${{ github.event.head_commit.message }}"
# Only tag versions on main branch
if [ "$GITHUB_REF" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
if [ "${{ github.ref }}" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
VERSION="${BASH_REMATCH[1]}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
@@ -281,30 +277,3 @@ jobs:
if: needs.check-docs-changes.outputs.docs_changed == 'true'
uses: ./.github/workflows/docs-embeddings.yml
secrets: inherit
# Create GitHub Release (only for version commits on main, after all builds complete)
create-release:
name: Create GitHub Release
runs-on: blacksmith-4vcpu-ubuntu-2404
needs: [create-ghcr-manifests, detect-version]
if: needs.detect-version.outputs.is_release == 'true'
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Create release
env:
GH_PAT: ${{ secrets.GITHUB_TOKEN }}
run: bun run scripts/create-single-release.ts ${{ needs.detect-version.outputs.version }}

View File

@@ -4,9 +4,6 @@ on:
workflow_call:
workflow_dispatch: # Allow manual triggering
permissions:
contents: read
jobs:
process-docs-embeddings:
name: Process Documentation Embeddings
@@ -20,7 +17,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.3
- name: Setup Node
uses: actions/setup-node@v4

View File

@@ -1,7 +1,11 @@
name: 'Auto-translate Documentation'
on:
workflow_dispatch: # Manual trigger only (scheduled runs disabled)
push:
branches: [ staging ]
paths:
- 'apps/docs/content/docs/en/**'
- 'apps/docs/i18n.json'
permissions:
contents: write
@@ -16,14 +20,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: staging
token: ${{ secrets.GH_PAT }}
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.3
- name: Cache Bun dependencies
uses: actions/cache@v4
@@ -65,11 +68,12 @@ jobs:
title: "feat(i18n): update translations"
body: |
## Summary
Automated weekly translation updates for documentation.
This PR was automatically created by the scheduled weekly i18n workflow, updating translations for all supported languages using Lingo.dev AI translation engine.
**Triggered**: Weekly scheduled run
Automated translation updates triggered by changes to documentation.
This PR was automatically created after content changes were made, updating translations for all supported languages using Lingo.dev AI translation engine.
**Original trigger**: ${{ github.event.head_commit.message }}
**Commit**: ${{ github.sha }}
**Workflow**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
## Type of Change
@@ -103,7 +107,7 @@ jobs:
## Screenshots/Videos
<!-- Translation changes are text-based - no visual changes expected -->
<!-- Reviewers should check the documentation site renders correctly for all languages -->
branch: auto-translate/weekly-${{ github.run_id }}
branch: auto-translate/staging-merge-${{ github.run_id }}
base: staging
labels: |
i18n
@@ -122,7 +126,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.3
- name: Cache Bun dependencies
uses: actions/cache@v4
@@ -141,8 +145,6 @@ jobs:
bun install --frozen-lockfile
- name: Build documentation to verify translations
env:
DATABASE_URL: postgresql://dummy:dummy@localhost:5432/dummy
run: |
cd apps/docs
bun run build
@@ -151,7 +153,7 @@ jobs:
run: |
cd apps/docs
echo "## Translation Status Report" >> $GITHUB_STEP_SUMMARY
echo "**Weekly scheduled translation run**" >> $GITHUB_STEP_SUMMARY
echo "**Triggered by merge to staging branch**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
en_count=$(find content/docs/en -name "*.mdx" | wc -l)

View File

@@ -146,7 +146,7 @@ jobs:
create-ghcr-manifests:
name: Create GHCR Manifests
runs-on: blacksmith-2vcpu-ubuntu-2404
runs-on: blacksmith-8vcpu-ubuntu-2404
needs: [build-amd64, build-ghcr-arm64]
if: github.ref == 'refs/heads/main'
strategy:

View File

@@ -4,9 +4,6 @@ on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
jobs:
migrate:
name: Apply Database Migrations
@@ -19,7 +16,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.3
- name: Cache Bun dependencies
uses: actions/cache@v4

View File

@@ -6,9 +6,6 @@ on:
paths:
- 'packages/cli/**'
permissions:
contents: read
jobs:
publish-npm:
runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -19,7 +16,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.3
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4

View File

@@ -6,9 +6,6 @@ on:
paths:
- 'packages/python-sdk/**'
permissions:
contents: write
jobs:
publish-pypi:
runs-on: blacksmith-4vcpu-ubuntu-2404

View File

@@ -6,9 +6,6 @@ on:
paths:
- 'packages/ts-sdk/**'
permissions:
contents: write
jobs:
publish-npm:
runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -19,7 +16,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.3
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4

View File

@@ -4,13 +4,10 @@ on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
jobs:
test-build:
name: Test and Build
runs-on: blacksmith-8vcpu-ubuntu-2404
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code
@@ -19,7 +16,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
bun-version: 1.3.3
- name: Setup Node
uses: actions/setup-node@v4
@@ -38,20 +35,6 @@ jobs:
key: ${{ github.repository }}-node-modules
path: ./node_modules
- name: Mount Turbo cache (Sticky Disk)
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-turbo-cache
path: ./.turbo
- name: Restore Next.js build cache
uses: actions/cache@v4
with:
path: ./apps/sim/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-nextjs-
- name: Install dependencies
run: bun install --frozen-lockfile
@@ -90,16 +73,6 @@ jobs:
echo "✅ All feature flags are properly configured"
- name: Check subblock ID stability
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="origin/${{ github.base_ref }}"
git fetch --depth=1 origin "${{ github.base_ref }}" 2>/dev/null || true
else
BASE_REF="HEAD~1"
fi
bun run apps/sim/scripts/check-subblock-id-stability.ts "$BASE_REF"
- name: Lint code
run: bun run lint:check
@@ -109,7 +82,6 @@ jobs:
NEXT_PUBLIC_APP_URL: 'https://www.sim.ai'
DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/simstudio'
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
TURBO_CACHE_DIR: .turbo
run: bun run test
- name: Check schema and migrations are in sync
@@ -135,8 +107,7 @@ jobs:
RESEND_API_KEY: 'dummy_key_for_ci_only'
AWS_REGION: 'us-west-2'
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
TURBO_CACHE_DIR: .turbo
run: bunx turbo run build --filter=sim
run: bun run build
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5

7
.gitignore vendored
View File

@@ -26,9 +26,6 @@ bun-debug.log*
**/standalone/
sim-standalone.tar.gz
# redis
dump.rdb
# misc
.DS_Store
*.pem
@@ -76,7 +73,3 @@ start-collector.sh
## Helm Chart Tests
helm/sim/test
i18n.cache
## Claude Code
.claude/launch.json
.claude/worktrees/

118
CLAUDE.md
View File

@@ -134,64 +134,21 @@ Use `devtools` middleware. Use `persist` only when data should survive reload wi
## React Query
All React Query hooks live in `hooks/queries/`. All server state must go through React Query — never use `useState` + `fetch` in components for data fetching or mutations.
### Query Key Factory
Every file must have a hierarchical key factory with an `all` root key and intermediate plural keys for prefix invalidation:
All React Query hooks live in `hooks/queries/`.
```typescript
export const entityKeys = {
all: ['entity'] as const,
lists: () => [...entityKeys.all, 'list'] as const,
list: (workspaceId?: string) => [...entityKeys.lists(), workspaceId ?? ''] as const,
details: () => [...entityKeys.all, 'detail'] as const,
detail: (id?: string) => [...entityKeys.details(), id ?? ''] as const,
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] as const,
}
```
### Query Hooks
- Every `queryFn` must forward `signal` for request cancellation
- Every query must have an explicit `staleTime`
- Use `keepPreviousData` only on variable-key queries (where params change), never on static keys
```typescript
export function useEntityList(workspaceId?: string) {
return useQuery({
queryKey: entityKeys.list(workspaceId),
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
queryFn: () => fetchEntities(workspaceId as string),
enabled: Boolean(workspaceId),
staleTime: 60 * 1000,
placeholderData: keepPreviousData, // OK: workspaceId varies
})
}
```
### Mutation Hooks
- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible
- For optimistic updates: use `onSettled` (not `onSuccess`) for cache reconciliation — `onSettled` fires on both success and error
- Don't include mutation objects in `useCallback` deps — `.mutate()` is stable in TanStack Query v5
```typescript
export function useUpdateEntity() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (variables) => { /* ... */ },
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey: entityKeys.detail(variables.id) })
const previous = queryClient.getQueryData(entityKeys.detail(variables.id))
queryClient.setQueryData(entityKeys.detail(variables.id), /* optimistic */)
return { previous }
},
onError: (_err, variables, context) => {
queryClient.setQueryData(entityKeys.detail(variables.id), context?.previous)
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
queryClient.invalidateQueries({ queryKey: entityKeys.detail(variables.id) })
},
placeholderData: keepPreviousData,
})
}
```
@@ -210,51 +167,27 @@ Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA
## Testing
Use Vitest. Test files: `feature.ts``feature.test.ts`. See `.cursor/rules/sim-testing.mdc` for full details.
### Global Mocks (vitest.setup.ts)
`@sim/db`, `drizzle-orm`, `@sim/logger`, `@/blocks/registry`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior.
### Standard Test Pattern
Use Vitest. Test files: `feature.ts``feature.test.ts`
```typescript
/**
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { databaseMock, loggerMock } from '@sim/testing'
import { describe, expect, it, vi } from 'vitest'
const { mockGetSession } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
}))
vi.mock('@sim/db', () => databaseMock)
vi.mock('@sim/logger', () => loggerMock)
vi.mock('@/lib/auth', () => ({
auth: { api: { getSession: vi.fn() } },
getSession: mockGetSession,
}))
import { myFunction } from '@/lib/feature'
import { GET } from '@/app/api/my-route/route'
describe('my route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
})
it('returns data', async () => { ... })
describe('feature', () => {
beforeEach(() => vi.clearAllMocks())
it.concurrent('runs in parallel', () => { ... })
})
```
### Performance Rules
- **NEVER** use `vi.resetModules()` + `vi.doMock()` + `await import()` — use `vi.hoisted()` + `vi.mock()` + static imports
- **NEVER** use `vi.importActual()` — mock everything explicitly
- **NEVER** use `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` from `@sim/testing` — they use `vi.doMock()` internally
- **Mock heavy deps** (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
- **Use `@vitest-environment node`** unless DOM APIs are needed (`window`, `document`, `FormData`)
- **Avoid real timers** — use 1ms delays or `vi.useFakeTimers()`
Use `@sim/testing` mocks/factories over local test data.
Use `@sim/testing` mocks/factories over local test data. See `.cursor/rules/sim-testing.mdc` for details.
## Utils Rules
@@ -305,7 +238,7 @@ export const ServiceBlock: BlockConfig = {
bgColor: '#hexcolor',
icon: ServiceIcon,
subBlocks: [ /* see SubBlock Properties */ ],
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}`, params: (p) => ({ /* type coercions here */ }) } },
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}` } },
inputs: { /* ... */ },
outputs: { /* ... */ },
}
@@ -313,8 +246,6 @@ export const ServiceBlock: BlockConfig = {
Register in `blocks/registry.ts` (alphabetically).
**Important:** `tools.config.tool` runs during serialization (before variable resolution). Never do `Number()` or other type coercions there — dynamic references like `<Block.output>` will be destroyed. Use `tools.config.params` for type coercions (it runs during execution, after variables are resolved).
**SubBlock Properties:**
```typescript
{
@@ -334,23 +265,6 @@ Register in `blocks/registry.ts` (alphabetically).
**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }`
**File Input Pattern (basic/advanced mode):**
```typescript
// Basic: file-upload UI
{ id: 'uploadFile', type: 'file-upload', canonicalParamId: 'file', mode: 'basic' },
// Advanced: reference from other blocks
{ id: 'fileRef', type: 'short-input', canonicalParamId: 'file', mode: 'advanced' },
```
In `tools.config.tool`, normalize with:
```typescript
import { normalizeFileInput } from '@/blocks/utils'
const file = normalizeFileInput(params.uploadFile || params.fileRef, { single: true })
if (file) params.file = file
```
For file uploads, create an internal API route (`/api/tools/{service}/upload`) that uses `downloadFileFromStorage` to get file content from `UserFile` objects.
### 3. Icon (`components/icons.tsx`)
```typescript
@@ -379,5 +293,3 @@ Register in `triggers/registry.ts`.
- [ ] Create block in `blocks/blocks/{service}.ts`
- [ ] Register block in `blocks/registry.ts`
- [ ] (Optional) Create and register triggers
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
- [ ] (If file uploads) Use `normalizeFileInput` in block config

View File

@@ -4,17 +4,17 @@
</a>
</p>
<p align="center">The open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to orchestrate agentic workflows.</p>
<p align="center">Build and deploy AI agent workflows in minutes.</p>
<p align="center">
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-6F3DFA" alt="Sim.ai"></a>
<a href="https://discord.gg/Hr4UWYEcTT" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
<a href="https://x.com/simdotai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/twitter/follow/simdotai?style=social" alt="Twitter"></a>
<a href="https://x.com/simdotai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/twitter/follow/simstudioai?style=social" alt="Twitter"></a>
<a href="https://docs.sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Docs-6F3DFA.svg" alt="Documentation"></a>
</p>
<p align="center">
<a href="https://deepwiki.com/simstudioai/sim" target="_blank" rel="noopener noreferrer"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a> <a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-000000?logo=cursor&logoColor=white" alt="Set Up with Cursor"></a>
<a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20Studio%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-000000?logo=cursor&logoColor=white" alt="Set Up with Cursor"></a>
</p>
### Build Workflows with Ease
@@ -172,6 +172,31 @@ Key environment variables for self-hosted deployments. See [`.env.example`](apps
| `API_ENCRYPTION_KEY` | Yes | Encrypts API keys (`openssl rand -hex 32`) |
| `COPILOT_API_KEY` | No | API key from sim.ai for Copilot features |
## Troubleshooting
### Ollama models not showing in dropdown (Docker)
If you're running Ollama on your host machine and Sim in Docker, change `OLLAMA_URL` from `localhost` to `host.docker.internal`:
```bash
OLLAMA_URL=http://host.docker.internal:11434 docker compose -f docker-compose.prod.yml up -d
```
See [Using an External Ollama Instance](#using-an-external-ollama-instance) for details.
### Database connection issues
Ensure PostgreSQL has the pgvector extension installed. When using Docker, wait for the database to be healthy before running migrations.
### Port conflicts
If ports 3000, 3002, or 5432 are in use, configure alternatives:
```bash
# Custom ports
NEXT_PUBLIC_APP_URL=http://localhost:3100 POSTGRES_PORT=5433 docker compose up -d
```
## Tech Stack
- **Framework**: [Next.js](https://nextjs.org/) (App Router)

View File

@@ -1,9 +1,5 @@
import type React from 'react'
import type { Root } from 'fumadocs-core/page-tree'
import { findNeighbour } from 'fumadocs-core/page-tree'
import type { ApiPageProps } from 'fumadocs-openapi/ui'
import { createAPIPage } from 'fumadocs-openapi/ui'
import { Pre } from 'fumadocs-ui/components/codeblock'
import defaultMdxComponents from 'fumadocs-ui/mdx'
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'
import { ChevronLeft, ChevronRight } from 'lucide-react'
@@ -15,75 +11,27 @@ import { LLMCopyButton } from '@/components/page-actions'
import { StructuredData } from '@/components/structured-data'
import { CodeBlock } from '@/components/ui/code-block'
import { Heading } from '@/components/ui/heading'
import { ResponseSection } from '@/components/ui/response-section'
import { i18n } from '@/lib/i18n'
import { getApiSpecContent, openapi } from '@/lib/openapi'
import { type PageData, source } from '@/lib/source'
const SUPPORTED_LANGUAGES: Set<string> = new Set(i18n.languages)
const BASE_URL = 'https://docs.sim.ai'
function resolveLangAndSlug(params: { slug?: string[]; lang: string }) {
const isValidLang = SUPPORTED_LANGUAGES.has(params.lang)
const lang = isValidLang ? params.lang : 'en'
const slug = isValidLang ? params.slug : [params.lang, ...(params.slug ?? [])]
return { lang, slug }
}
const APIPage = createAPIPage(openapi, {
playground: { enabled: false },
content: {
renderOperationLayout: async (slots) => {
return (
<div className='flex @4xl:flex-row flex-col @4xl:items-start gap-x-6 gap-y-4'>
<div className='min-w-0 flex-1'>
{slots.header}
{slots.apiPlayground}
{slots.authSchemes && <div className='api-section-divider'>{slots.authSchemes}</div>}
{slots.paremeters}
{slots.body && <div className='api-section-divider'>{slots.body}</div>}
<ResponseSection>{slots.responses}</ResponseSection>
{slots.callbacks}
</div>
<div className='@4xl:sticky @4xl:top-[calc(var(--fd-docs-row-1,2rem)+1rem)] @4xl:w-[400px]'>
{slots.apiExample}
</div>
</div>
)
},
},
})
export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) {
const params = await props.params
const { lang, slug } = resolveLangAndSlug(params)
const page = source.getPage(slug, lang)
const page = source.getPage(params.slug, params.lang)
if (!page) notFound()
const data = page.data as unknown as PageData & {
_openapi?: { method?: string }
getAPIPageProps?: () => ApiPageProps
}
const isOpenAPI = '_openapi' in data && data._openapi != null
const isApiReference = slug?.some((s) => s === 'api-reference') ?? false
const data = page.data as PageData
const MDX = data.body
const baseUrl = 'https://docs.sim.ai'
const pageTreeRecord = source.pageTree as Record<string, Root>
const pageTree = pageTreeRecord[lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
const rawNeighbours = pageTree ? findNeighbour(pageTree, page.url) : null
const neighbours = isApiReference
? {
previous: rawNeighbours?.previous?.url.includes('/api-reference/')
? rawNeighbours.previous
: undefined,
next: rawNeighbours?.next?.url.includes('/api-reference/') ? rawNeighbours.next : undefined,
}
: rawNeighbours
const pageTreeRecord = source.pageTree as Record<string, any>
const pageTree =
pageTreeRecord[params.lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
const neighbours = pageTree ? findNeighbour(pageTree, page.url) : null
const generateBreadcrumbs = () => {
const breadcrumbs: Array<{ name: string; url: string }> = [
{
name: 'Home',
url: BASE_URL,
url: baseUrl,
},
]
@@ -91,7 +39,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
let currentPath = ''
urlParts.forEach((part, index) => {
if (index === 0 && SUPPORTED_LANGUAGES.has(part)) {
if (index === 0 && ['en', 'es', 'fr', 'de', 'ja', 'zh'].includes(part)) {
currentPath = `/${part}`
return
}
@@ -106,12 +54,12 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
if (index === urlParts.length - 1) {
breadcrumbs.push({
name: data.title,
url: `${BASE_URL}${page.url}`,
url: `${baseUrl}${page.url}`,
})
} else {
breadcrumbs.push({
name: name,
url: `${BASE_URL}${currentPath}`,
url: `${baseUrl}${currentPath}`,
})
}
})
@@ -123,6 +71,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
const CustomFooter = () => (
<div className='mt-12'>
{/* Navigation links */}
<div className='flex items-center justify-between py-8'>
{neighbours?.previous ? (
<Link
@@ -149,8 +98,10 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
)}
</div>
{/* Divider line */}
<div className='border-border border-t' />
{/* Social icons */}
<div className='flex items-center gap-4 py-6'>
<Link
href='https://x.com/simdotai'
@@ -216,70 +167,13 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
</div>
)
if (isOpenAPI && data.getAPIPageProps) {
const apiProps = data.getAPIPageProps()
const apiPageContent = getApiSpecContent(
data.title,
data.description,
apiProps.operations ?? []
)
return (
<>
<StructuredData
title={data.title}
description={data.description || ''}
url={`${BASE_URL}${page.url}`}
lang={lang}
breadcrumb={breadcrumbs}
/>
<style>{`#nd-page { grid-column: main-start / toc-end !important; max-width: 1400px !important; }`}</style>
<DocsPage
toc={data.toc}
breadcrumb={{
enabled: false,
}}
tableOfContent={{
style: 'clerk',
enabled: false,
}}
tableOfContentPopover={{
style: 'clerk',
enabled: false,
}}
footer={{
enabled: true,
component: <CustomFooter />,
}}
>
<div className='api-page-header relative mt-6 sm:mt-0'>
<div className='absolute top-1 right-0 flex items-center gap-2'>
<div className='hidden sm:flex'>
<LLMCopyButton content={apiPageContent} />
</div>
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
</div>
<DocsTitle>{data.title}</DocsTitle>
<DocsDescription>{data.description}</DocsDescription>
</div>
<DocsBody>
<APIPage {...apiProps} />
</DocsBody>
</DocsPage>
</>
)
}
const MDX = data.body
const markdownContent = await data.getText('processed')
return (
<>
<StructuredData
title={data.title}
description={data.description || ''}
url={`${BASE_URL}${page.url}`}
lang={lang}
url={`${baseUrl}${page.url}`}
lang={params.lang}
breadcrumb={breadcrumbs}
/>
<DocsPage
@@ -291,6 +185,11 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
tableOfContent={{
style: 'clerk',
enabled: true,
header: (
<div key='toc-header' className='mb-2 font-medium text-sm'>
On this page
</div>
),
footer: <TOCFooter />,
single: false,
}}
@@ -306,7 +205,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
<div className='relative mt-6 sm:mt-0'>
<div className='absolute top-1 right-0 flex items-center gap-2'>
<div className='hidden sm:flex'>
<LLMCopyButton content={markdownContent} />
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
</div>
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
</div>
@@ -317,11 +216,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
<MDX
components={{
...defaultMdxComponents,
pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
<CodeBlock {...props}>
<Pre>{props.children}</Pre>
</CodeBlock>
),
CodeBlock,
h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
<Heading as='h1' {...props} />
),
@@ -356,29 +251,27 @@ export async function generateMetadata(props: {
params: Promise<{ slug?: string[]; lang: string }>
}) {
const params = await props.params
const { lang, slug } = resolveLangAndSlug(params)
const page = source.getPage(slug, lang)
const page = source.getPage(params.slug, params.lang)
if (!page) notFound()
const data = page.data as unknown as PageData
const fullUrl = `${BASE_URL}${page.url}`
const data = page.data as PageData
const baseUrl = 'https://docs.sim.ai'
const fullUrl = `${baseUrl}${page.url}`
const ogImageUrl = `${BASE_URL}/api/og?title=${encodeURIComponent(data.title)}`
const ogImageUrl = `${baseUrl}/api/og?title=${encodeURIComponent(data.title)}`
return {
title: data.title,
description:
data.description ||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
data.description || 'Sim visual workflow builder for AI applications documentation',
keywords: [
'AI agents',
'agentic workforce',
'AI agent platform',
'agentic workflows',
'LLM orchestration',
'AI workflow builder',
'visual workflow editor',
'AI automation',
'knowledge base',
'AI integrations',
'workflow automation',
'AI agents',
'no-code AI',
'drag and drop workflows',
data.title?.toLowerCase().split(' '),
]
.flat()
@@ -388,15 +281,14 @@ export async function generateMetadata(props: {
openGraph: {
title: data.title,
description:
data.description ||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
data.description || 'Sim visual workflow builder for AI applications documentation',
url: fullUrl,
siteName: 'Sim Documentation',
type: 'article',
locale: lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`,
locale: params.lang === 'en' ? 'en_US' : `${params.lang}_${params.lang.toUpperCase()}`,
alternateLocale: ['en', 'es', 'fr', 'de', 'ja', 'zh']
.filter((l) => l !== lang)
.map((l) => (l === 'en' ? 'en_US' : `${l}_${l.toUpperCase()}`)),
.filter((lang) => lang !== params.lang)
.map((lang) => (lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`)),
images: [
{
url: ogImageUrl,
@@ -410,8 +302,7 @@ export async function generateMetadata(props: {
card: 'summary_large_image',
title: data.title,
description:
data.description ||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
data.description || 'Sim visual workflow builder for AI applications documentation',
images: [ogImageUrl],
creator: '@simdotai',
site: '@simdotai',
@@ -431,13 +322,13 @@ export async function generateMetadata(props: {
alternates: {
canonical: fullUrl,
languages: {
'x-default': `${BASE_URL}${page.url.replace(`/${lang}`, '')}`,
en: `${BASE_URL}${page.url.replace(`/${lang}`, '')}`,
es: `${BASE_URL}/es${page.url.replace(`/${lang}`, '')}`,
fr: `${BASE_URL}/fr${page.url.replace(`/${lang}`, '')}`,
de: `${BASE_URL}/de${page.url.replace(`/${lang}`, '')}`,
ja: `${BASE_URL}/ja${page.url.replace(`/${lang}`, '')}`,
zh: `${BASE_URL}/zh${page.url.replace(`/${lang}`, '')}`,
'x-default': `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`,
en: `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`,
es: `${baseUrl}/es${page.url.replace(`/${params.lang}`, '')}`,
fr: `${baseUrl}/fr${page.url.replace(`/${params.lang}`, '')}`,
de: `${baseUrl}/de${page.url.replace(`/${params.lang}`, '')}`,
ja: `${baseUrl}/ja${page.url.replace(`/${params.lang}`, '')}`,
zh: `${baseUrl}/zh${page.url.replace(`/${params.lang}`, '')}`,
},
},
}

View File

@@ -3,15 +3,13 @@ import { defineI18nUI } from 'fumadocs-ui/i18n'
import { DocsLayout } from 'fumadocs-ui/layouts/docs'
import { RootProvider } from 'fumadocs-ui/provider/next'
import { Geist_Mono, Inter } from 'next/font/google'
import Script from 'next/script'
import Image from 'next/image'
import {
SidebarFolder,
SidebarItem,
SidebarSeparator,
} from '@/components/docs-layout/sidebar-components'
import { Navbar } from '@/components/navbar/navbar'
import { AnimatedBlocks } from '@/components/ui/animated-blocks'
import { SimLogoFull } from '@/components/ui/sim-logo'
import { i18n } from '@/lib/i18n'
import { source } from '@/lib/source'
import '../global.css'
@@ -19,13 +17,11 @@ import '../global.css'
const inter = Inter({
subsets: ['latin'],
variable: '--font-geist-sans',
display: 'swap',
})
const geistMono = Geist_Mono({
subsets: ['latin'],
variable: '--font-geist-mono',
display: 'swap',
})
const { provider } = defineI18nUI(i18n, {
@@ -56,18 +52,15 @@ type LayoutProps = {
params: Promise<{ lang: string }>
}
const SUPPORTED_LANGUAGES: Set<string> = new Set(i18n.languages)
export default async function Layout({ children, params }: LayoutProps) {
const { lang: rawLang } = await params
const lang = SUPPORTED_LANGUAGES.has(rawLang) ? rawLang : 'en'
const { lang } = await params
const structuredData = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Sim Documentation',
description:
'Documentation for Sim the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
'Comprehensive documentation for Sim - the visual workflow builder for AI Agent Workflows.',
url: 'https://docs.sim.ai',
publisher: {
'@type': 'Organization',
@@ -100,19 +93,27 @@ export default async function Layout({ children, params }: LayoutProps) {
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
{/* OneDollarStats Analytics - CDN script handles everything automatically */}
<script defer src='https://assets.onedollarstats.com/stonks.js' />
</head>
<body className='flex min-h-screen flex-col font-sans'>
<Script src='https://assets.onedollarstats.com/stonks.js' strategy='lazyOnload' />
<AnimatedBlocks />
<RootProvider i18n={provider(lang)}>
<Navbar />
<DocsLayout
tree={source.pageTree[lang]}
nav={{
title: <SimLogoFull className='h-7 w-auto' />,
title: (
<Image
src='/static/logo.png'
alt='Sim'
width={72}
height={28}
className='h-7 w-auto'
priority
/>
),
}}
sidebar={{
tabs: false,
defaultOpenLevel: 0,
collapsible: false,
footer: null,

View File

@@ -9,7 +9,7 @@ export default function NotFound() {
<DocsPage>
<DocsBody>
<div className='flex min-h-[60vh] flex-col items-center justify-center text-center'>
<h1 className='mb-4 bg-gradient-to-b from-[#47d991] to-[#33c482] bg-clip-text font-bold text-8xl text-transparent'>
<h1 className='mb-4 bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] bg-clip-text font-bold text-8xl text-transparent'>
404
</h1>
<h2 className='mb-2 font-semibold text-2xl text-foreground'>Page Not Found</h2>

View File

@@ -33,41 +33,15 @@ async function loadGoogleFont(font: string, weights: string, text: string): Prom
throw new Error('Failed to load font data')
}
/**
* Sim logo with icon and "Sim" text for OG image.
*/
function SimLogoFull() {
return (
<svg height='28' viewBox='720 440 1020 320' fill='none'>
{/* Green icon - top left shape with cutout */}
<path
fillRule='evenodd'
clipRule='evenodd'
d='M875.791 577.171C875.791 581.922 873.911 586.483 870.576 589.842L870.098 590.323C866.764 593.692 862.234 595.575 857.517 595.575H750.806C740.978 595.575 733 603.6 733 613.498V728.902C733 738.799 740.978 746.826 750.806 746.826H865.382C875.209 746.826 883.177 738.799 883.177 728.902V620.853C883.177 616.448 884.912 612.222 888.008 609.104C891.093 605.997 895.29 604.249 899.664 604.249H1008.16C1017.99 604.249 1025.96 596.224 1025.96 586.327V470.923C1025.96 461.025 1017.99 453 1008.16 453H893.586C883.759 453 875.791 461.025 875.791 470.923V577.171ZM910.562 477.566H991.178C996.922 477.566 1001.57 482.254 1001.57 488.029V569.22C1001.57 574.995 996.922 579.683 991.178 579.683H910.562C904.828 579.683 900.173 574.995 900.173 569.22V488.029C900.173 482.254 904.828 477.566 910.562 477.566Z'
fill='#33C482'
/>
{/* Green icon - bottom right square */}
<path
d='M1008.3 624.59H923.113C912.786 624.59 904.414 633.022 904.414 643.423V728.171C904.414 738.572 912.786 747.004 923.113 747.004H1008.3C1018.63 747.004 1027 738.572 1027 728.171V643.423C1027 633.022 1018.63 624.59 1008.3 624.59Z'
fill='#33C482'
/>
{/* "Sim" text - white for dark background */}
<path
d='M1210.54 515.657C1226.65 515.657 1240.59 518.51 1252.31 524.257H1252.31C1264.3 529.995 1273.63 538.014 1280.26 548.319H1280.26C1287.19 558.635 1290.78 570.899 1291.08 585.068L1291.1 586.089H1249.11L1249.09 585.115C1248.8 574.003 1245.18 565.493 1238.32 559.451C1231.45 553.399 1221.79 550.308 1209.21 550.308C1196.3 550.308 1186.48 553.113 1179.61 558.588C1172.76 564.046 1169.33 571.499 1169.33 581.063C1169.33 588.092 1171.88 593.978 1177.01 598.783C1182.17 603.618 1189.99 607.399 1200.56 610.061H1200.56L1238.77 619.451C1257.24 623.65 1271.21 630.571 1280.57 640.293L1281.01 640.739C1290.13 650.171 1294.64 662.97 1294.64 679.016C1294.64 692.923 1290.88 705.205 1283.34 715.822L1283.33 715.834C1275.81 726.134 1265.44 734.14 1252.26 739.866L1252.25 739.871C1239.36 745.302 1224.12 748 1206.54 748C1180.9 748 1160.36 741.696 1145.02 728.984C1129.67 716.258 1122 699.269 1122 678.121V677.121H1163.99V678.121C1163.99 688.869 1167.87 697.367 1175.61 703.722L1176.34 704.284C1184.04 709.997 1194.37 712.902 1207.43 712.902C1222.13 712.902 1233.3 710.087 1241.07 704.588C1248.8 698.812 1252.64 691.21 1252.64 681.699C1252.64 674.769 1250.5 669.057 1246.25 664.49L1246.23 664.478L1246.22 664.464C1242.28 659.929 1234.83 656.119 1223.64 653.152L1185.43 644.208L1185.42 644.204C1166.05 639.407 1151.49 632.035 1141.83 622.012L1141.83 622.006L1141.82 622C1132.43 611.94 1127.78 598.707 1127.78 582.405C1127.78 568.81 1131.23 556.976 1138.17 546.949L1138.18 546.941L1138.19 546.933C1145.41 536.936 1155.18 529.225 1167.48 523.793L1167.48 523.79C1180.07 518.36 1194.43 515.657 1210.54 515.657ZM1323.39 521.979C1331.68 525.008 1337.55 526.482 1343.51 526.482C1349.48 526.482 1355.64 525.005 1364.49 521.973L1365.82 521.52V742.633H1322.05V521.489L1323.39 521.979ZM1642.01 515.657C1667.11 515.657 1686.94 523.031 1701.39 537.876C1715.83 552.716 1723 572.968 1723 598.507V742.633H1680.12V608.794C1680.12 591.666 1675.72 578.681 1667.07 569.681L1667.06 569.669L1667.04 569.656C1658.67 560.359 1647.26 555.675 1632.68 555.675C1622.47 555.675 1613.47 558.022 1605.64 562.69L1605.63 562.696C1598.11 567.064 1592.17 573.475 1587.8 581.968C1583.44 590.448 1581.25 600.424 1581.25 611.925V742.633H1537.92V608.347C1537.92 591.208 1533.67 578.376 1525.31 569.68L1525.31 569.674L1525.3 569.668C1516.93 560.664 1505.52 556.122 1490.93 556.122C1480.72 556.122 1471.72 558.469 1463.89 563.138L1463.88 563.144C1456.36 567.511 1450.41 573.922 1446.05 582.415L1446.05 582.422L1446.04 582.428C1441.69 590.602 1439.5 600.423 1439.5 611.925V742.633H1395.72V521.919H1435.05V554.803C1439.92 544.379 1447.91 535.465 1458.37 528.356C1470.71 519.875 1485.58 515.657 1502.93 515.657C1522.37 515.657 1538.61 520.931 1551.55 531.538C1560.38 538.771 1567.1 547.628 1571.72 558.091C1576.05 547.619 1582.83 538.757 1592.07 531.524C1605.61 520.93 1622.28 515.657 1642.01 515.657ZM1343.49 452C1351.45 452 1358.23 454.786 1363.75 460.346C1369.27 465.905 1372.04 472.721 1372.04 480.73C1372.04 488.452 1369.27 495.254 1363.77 501.096L1363.76 501.105L1363.75 501.115C1358.23 506.675 1351.45 509.461 1343.49 509.461C1335.81 509.461 1329.05 506.669 1323.25 501.134L1323.23 501.115L1323.21 501.096C1317.71 495.254 1314.94 488.452 1314.94 480.73C1314.94 472.721 1317.7 465.905 1323.23 460.346L1323.24 460.337L1323.25 460.327C1329.05 454.792 1335.81 452 1343.49 452Z'
fill='#fafafa'
/>
</svg>
)
}
/**
* Generates dynamic Open Graph images for documentation pages.
* Style matches Cursor docs: dark background, title at top, logo bottom-left, domain bottom-right.
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title') || 'Documentation'
const baseUrl = new URL(request.url).origin
const allText = `${title}docs.sim.ai`
const fontData = await loadGoogleFont('Geist', '400;500;600', allText)
@@ -78,39 +52,84 @@ export async function GET(request: NextRequest) {
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '56px 64px',
background: '#121212', // Dark mode background matching docs (hsla 0, 0%, 7%)
background: '#0c0c0c',
position: 'relative',
fontFamily: 'Geist',
}}
>
{/* Title at top */}
<span
{/* Base gradient layer - subtle purple tint across the entire image */}
<div
style={{
fontSize: getTitleFontSize(title),
fontWeight: 500,
color: '#fafafa', // Light text matching docs
lineHeight: 1.2,
letterSpacing: '-0.02em',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background:
'radial-gradient(ellipse 150% 100% at 50% 100%, rgba(88, 28, 135, 0.15) 0%, rgba(88, 28, 135, 0.08) 25%, rgba(88, 28, 135, 0.03) 50%, transparent 80%)',
display: 'flex',
}}
>
{title}
</span>
/>
{/* Footer: icon left, domain right */}
{/* Secondary glow - adds depth without harsh edges */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background:
'radial-gradient(ellipse 100% 80% at 80% 90%, rgba(112, 31, 252, 0.12) 0%, rgba(112, 31, 252, 0.04) 40%, transparent 70%)',
display: 'flex',
}}
/>
{/* Top darkening - creates natural vignette */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background:
'linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, transparent 40%, transparent 100%)',
display: 'flex',
}}
/>
{/* Content */}
<div
style={{
display: 'flex',
flexDirection: 'column',
padding: '56px 72px',
height: '100%',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
}}
>
<SimLogoFull />
{/* Logo */}
<img src={`${baseUrl}/static/logo.png`} alt='sim' height={32} />
{/* Title */}
<span
style={{
fontSize: getTitleFontSize(title),
fontWeight: 600,
color: '#ffffff',
lineHeight: 1.1,
letterSpacing: '-0.02em',
}}
>
{title}
</span>
{/* Footer */}
<span
style={{
fontSize: 20,
fontWeight: 400,
fontWeight: 500,
color: '#71717a',
}}
>

View File

@@ -86,112 +86,27 @@ export async function GET(request: NextRequest) {
)
.limit(candidateLimit)
const knownLocales = ['en', 'es', 'fr', 'de', 'ja', 'zh']
const seenIds = new Set<string>()
const mergedResults = []
const vectorRankMap = new Map<string, number>()
vectorResults.forEach((r, idx) => vectorRankMap.set(r.chunkId, idx + 1))
const keywordRankMap = new Map<string, number>()
keywordResults.forEach((r, idx) => keywordRankMap.set(r.chunkId, idx + 1))
const allChunkIds = new Set([
...vectorResults.map((r) => r.chunkId),
...keywordResults.map((r) => r.chunkId),
])
const k = 60
type ResultWithRRF = (typeof vectorResults)[0] & { rrfScore: number }
const scoredResults: ResultWithRRF[] = []
for (const chunkId of allChunkIds) {
const vectorRank = vectorRankMap.get(chunkId) ?? Number.POSITIVE_INFINITY
const keywordRank = keywordRankMap.get(chunkId) ?? Number.POSITIVE_INFINITY
const rrfScore = 1 / (k + vectorRank) + 1 / (k + keywordRank)
const result =
vectorResults.find((r) => r.chunkId === chunkId) ||
keywordResults.find((r) => r.chunkId === chunkId)
if (result) {
scoredResults.push({ ...result, rrfScore })
for (let i = 0; i < Math.max(vectorResults.length, keywordResults.length); i++) {
if (i < vectorResults.length && !seenIds.has(vectorResults[i].chunkId)) {
mergedResults.push(vectorResults[i])
seenIds.add(vectorResults[i].chunkId)
}
if (i < keywordResults.length && !seenIds.has(keywordResults[i].chunkId)) {
mergedResults.push(keywordResults[i])
seenIds.add(keywordResults[i].chunkId)
}
}
scoredResults.sort((a, b) => b.rrfScore - a.rrfScore)
const localeFilteredResults = scoredResults.filter((result) => {
const firstPart = result.sourceDocument.split('/')[0]
if (knownLocales.includes(firstPart)) {
return firstPart === locale
}
return locale === 'en'
})
const queryLower = query.toLowerCase()
const getTitleBoost = (result: ResultWithRRF): number => {
const fileName = result.sourceDocument
.replace('.mdx', '')
.split('/')
.pop()
?.toLowerCase()
?.replace(/_/g, ' ')
if (fileName === queryLower) return 0.01
if (fileName?.includes(queryLower)) return 0.005
return 0
}
localeFilteredResults.sort((a, b) => {
return b.rrfScore + getTitleBoost(b) - (a.rrfScore + getTitleBoost(a))
})
const pageMap = new Map<string, ResultWithRRF>()
for (const result of localeFilteredResults) {
const pageKey = result.sourceDocument
const existing = pageMap.get(pageKey)
if (!existing || result.rrfScore > existing.rrfScore) {
pageMap.set(pageKey, result)
}
}
const deduplicatedResults = Array.from(pageMap.values())
.sort((a, b) => b.rrfScore + getTitleBoost(b) - (a.rrfScore + getTitleBoost(a)))
.slice(0, limit)
const searchResults = deduplicatedResults.map((result) => {
const filteredResults = mergedResults.slice(0, limit)
const searchResults = filteredResults.map((result) => {
const title = result.headerText || result.sourceDocument.replace('.mdx', '')
const pathParts = result.sourceDocument
.replace('.mdx', '')
.split('/')
.filter((part) => part !== 'index' && !knownLocales.includes(part))
.map((part) => {
return part
.replace(/_/g, ' ')
.split(' ')
.map((word) => {
const acronyms = [
'api',
'mcp',
'sdk',
'url',
'http',
'json',
'xml',
'html',
'css',
'ai',
]
if (acronyms.includes(word.toLowerCase())) {
return word.toUpperCase()
}
return word.charAt(0).toUpperCase() + word.slice(1)
})
.join(' ')
})
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
return {
id: result.chunkId,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'
/**
* Shared layout configurations
*
* you can customise layouts individually from:
* Home Layout: app/(home)/layout.tsx
* Docs Layout: app/docs/layout.tsx
*/
export const baseOptions: BaseLayoutProps = {
nav: {
title: (
<>
<svg width='24' height='24' xmlns='http://www.w3.org/2000/svg' aria-label='Logo'>
<circle cx={12} cy={12} r={12} fill='currentColor' />
</svg>
My App
</>
),
},
}

View File

@@ -7,27 +7,26 @@ export default function RootLayout({ children }: { children: ReactNode }) {
export const metadata = {
metadataBase: new URL('https://docs.sim.ai'),
title: {
default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
default: 'Sim Documentation - Visual Workflow Builder for AI Applications',
template: '%s',
},
description:
'Documentation for Sim the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.',
keywords: [
'AI agents',
'agentic workforce',
'AI agent platform',
'open-source AI agents',
'agentic workflows',
'LLM orchestration',
'AI integrations',
'knowledge base',
'AI workflow builder',
'visual workflow editor',
'AI automation',
'workflow builder',
'AI workflow orchestration',
'enterprise AI',
'AI agent deployment',
'intelligent automation',
'AI tools',
'workflow automation',
'AI agents',
'no-code AI',
'drag and drop workflows',
'AI integrations',
'workflow canvas',
'AI Agent Workflow Builder',
'workflow orchestration',
'agent builder',
'AI workflow automation',
'visual programming',
],
authors: [{ name: 'Sim Team', url: 'https://sim.ai' }],
creator: 'Sim',
@@ -54,9 +53,9 @@ export const metadata = {
alternateLocale: ['es_ES', 'fr_FR', 'de_DE', 'ja_JP', 'zh_CN'],
url: 'https://docs.sim.ai',
siteName: 'Sim Documentation',
title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
title: 'Sim Documentation - Visual Workflow Builder for AI Applications',
description:
'Documentation for Sim the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.',
images: [
{
url: 'https://docs.sim.ai/api/og?title=Sim%20Documentation',
@@ -68,9 +67,9 @@ export const metadata = {
},
twitter: {
card: 'summary_large_image',
title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
title: 'Sim Documentation - Visual Workflow Builder for AI Applications',
description:
'Documentation for Sim the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
'Comprehensive documentation for Sim - the visual workflow builder for AI applications.',
creator: '@simdotai',
site: '@simdotai',
images: ['https://docs.sim.ai/api/og?title=Sim%20Documentation'],

View File

@@ -37,9 +37,9 @@ export async function GET() {
const manifest = `# Sim Documentation
> The open-source platform to build AI agents and run your agentic workforce.
> Visual Workflow Builder for AI Applications
Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs. Trusted by over 100,000 builders.
Sim is a visual workflow builder for AI applications that lets you build AI agent workflows visually. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.
## Documentation Overview

View File

@@ -44,7 +44,7 @@ export function SidebarItem({ item }: { item: Item }) {
'lg:text-gray-600 lg:dark:text-gray-400',
!active && 'lg:hover:bg-gray-100/60 lg:dark:hover:bg-gray-800/40',
active &&
'lg:bg-emerald-50/80 lg:font-normal lg:text-emerald-600 lg:dark:bg-emerald-900/15 lg:dark:text-emerald-400'
'lg:bg-purple-50/80 lg:font-normal lg:text-purple-600 lg:dark:bg-purple-900/15 lg:dark:text-purple-400'
)}
>
{item.name}
@@ -52,26 +52,15 @@ export function SidebarItem({ item }: { item: Item }) {
)
}
function isApiReferenceFolder(node: Folder): boolean {
if (node.index?.url.includes('/api-reference/')) return true
for (const child of node.children) {
if (child.type === 'page' && child.url.includes('/api-reference/')) return true
if (child.type === 'folder' && isApiReferenceFolder(child)) return true
}
return false
}
export function SidebarFolder({ item, children }: { item: Folder; children: ReactNode }) {
const pathname = usePathname()
const hasActiveChild = checkHasActiveChild(item, pathname)
const isApiRef = isApiReferenceFolder(item)
const isOnApiRefPage = stripLangPrefix(pathname).startsWith('/api-reference')
const hasChildren = item.children.length > 0
const [open, setOpen] = useState(hasActiveChild || (isApiRef && isOnApiRefPage))
const [open, setOpen] = useState(hasActiveChild)
useEffect(() => {
setOpen(hasActiveChild || (isApiRef && isOnApiRefPage))
}, [hasActiveChild, isApiRef, isOnApiRefPage])
setOpen(hasActiveChild)
}, [hasActiveChild])
const active = item.index ? isActive(item.index.url, pathname, false) : false
@@ -90,7 +79,7 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac
'lg:text-gray-600 lg:dark:text-gray-400',
!active && 'lg:hover:bg-gray-100/60 lg:dark:hover:bg-gray-800/40',
active &&
'lg:bg-emerald-50/80 lg:font-normal lg:text-emerald-600 lg:dark:bg-emerald-900/15 lg:dark:text-emerald-400'
'lg:bg-purple-50/80 lg:font-normal lg:text-purple-600 lg:dark:bg-purple-900/15 lg:dark:text-purple-400'
)}
>
{item.name}
@@ -115,7 +104,7 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac
'lg:text-gray-800 lg:dark:text-gray-200',
!active && 'lg:hover:bg-gray-100/60 lg:dark:hover:bg-gray-800/40',
active &&
'lg:bg-emerald-50/80 lg:text-emerald-600 lg:dark:bg-emerald-900/15 lg:dark:text-emerald-400'
'lg:bg-purple-50/80 lg:text-purple-600 lg:dark:bg-purple-900/15 lg:dark:text-purple-400'
)}
>
{item.name}
@@ -168,18 +157,16 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac
{hasChildren && (
<div
className={cn(
'grid transition-[grid-template-rows,opacity] duration-200 ease-in-out',
open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
'overflow-hidden transition-all duration-200 ease-in-out',
open ? 'max-h-[10000px] opacity-100' : 'max-h-0 opacity-0'
)}
>
<div className='overflow-hidden'>
{/* Mobile: simple indent */}
<div className='ml-4 flex flex-col gap-0.5 lg:hidden'>{children}</div>
{/* Desktop: styled with border */}
<ul className='mt-0.5 ml-2 hidden space-y-[0.0625rem] border-gray-200/60 border-l pl-2.5 lg:block dark:border-gray-700/60'>
{children}
</ul>
</div>
{/* Mobile: simple indent */}
<div className='ml-4 flex flex-col gap-0.5 lg:hidden'>{children}</div>
{/* Desktop: styled with border */}
<ul className='mt-0.5 ml-2 hidden space-y-[0.0625rem] border-gray-200/60 border-l pl-2.5 lg:block dark:border-gray-700/60'>
{children}
</ul>
</div>
)}
</div>

View File

@@ -1,36 +1,38 @@
'use client'
import { useState } from 'react'
import { ArrowRight, ChevronRight } from 'lucide-react'
import Link from 'next/link'
export function TOCFooter() {
const [isHovered, setIsHovered] = useState(false)
return (
<div className='sticky bottom-0 mt-6'>
<div className='flex flex-col gap-2 rounded-lg border border-border bg-secondary p-6 text-sm'>
<div className='text-balance font-semibold text-base leading-tight'>
Start building today
</div>
<div className='text-muted-foreground'>Trusted by over 100,000 builders.</div>
<div className='text-muted-foreground'>Trusted by over 60,000 builders.</div>
<div className='text-muted-foreground'>
The open-source platform to build AI agents and run your agentic workforce.
Build Agentic workflows visually on a drag-and-drop canvas or with natural language.
</div>
<Link
href='https://sim.ai/signup'
target='_blank'
rel='noopener noreferrer'
className='group mt-2 inline-flex h-8 w-fit items-center justify-center gap-2 whitespace-nowrap rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-medium text-black text-sm outline-none transition-[filter] hover:brightness-110 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className='group mt-2 inline-flex h-8 w-fit items-center justify-center gap-1 whitespace-nowrap rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] px-3 pr-[10px] pl-[12px] font-medium text-sm text-white shadow-[inset_0_2px_4px_0_#9B77FF] outline-none transition-all hover:shadow-lg focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50'
aria-label='Get started with Sim - Sign up for free'
>
<span>Get started</span>
<span className='relative inline-flex h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5'>
<ChevronRight
className='absolute inset-0 h-4 w-4 transition-opacity duration-200 group-hover:opacity-0'
aria-hidden='true'
/>
<ArrowRight
className='absolute inset-0 h-4 w-4 opacity-0 transition-opacity duration-200 group-hover:opacity-100'
aria-hidden='true'
/>
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isHovered ? (
<ArrowRight className='h-4 w-4' aria-hidden='true' />
) : (
<ChevronRight className='h-4 w-4' aria-hidden='true' />
)}
</span>
</Link>
</div>

File diff suppressed because one or more lines are too long

View File

@@ -1,95 +1,65 @@
'use client'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { LanguageDropdown } from '@/components/ui/language-dropdown'
import { SearchTrigger } from '@/components/ui/search-trigger'
import { SimLogoFull } from '@/components/ui/sim-logo'
import { ThemeToggle } from '@/components/ui/theme-toggle'
import { cn } from '@/lib/utils'
const NAV_TABS = [
{
label: 'Documentation',
href: '/introduction',
match: (p: string) => !p.includes('/api-reference'),
external: false,
},
{
label: 'API Reference',
href: '/api-reference/getting-started',
match: (p: string) => p.includes('/api-reference'),
external: false,
},
{ label: 'Mothership', href: 'https://sim.ai', external: true },
] as const
export function Navbar() {
const pathname = usePathname()
return (
<nav className='sticky top-0 z-50 bg-background/80 backdrop-blur-md backdrop-saturate-150'>
<div className='hidden w-full flex-col lg:flex'>
{/* Top row: logo, search, controls */}
<nav
className='sticky top-0 z-50 border-border/50 border-b'
style={{
backdropFilter: 'blur(25px) saturate(180%)',
WebkitBackdropFilter: 'blur(25px) saturate(180%)',
}}
>
{/* Desktop: Single row layout */}
<div className='hidden h-16 w-full items-center lg:flex'>
<div
className='relative flex h-[52px] w-full items-center justify-between'
className='relative flex w-full items-center justify-between'
style={{
paddingLeft: 'calc(var(--sidebar-offset) + 32px)',
paddingRight: 'calc(var(--toc-offset) + 60px)',
}}
>
<Link href='/' className='flex min-w-[100px] items-center'>
<SimLogoFull className='h-7 w-auto' />
</Link>
{/* Left cluster: logo */}
<div className='flex items-center'>
<Link href='/' className='flex min-w-[100px] items-center'>
<Image
src='/static/logo.png'
alt='Sim'
width={72}
height={28}
className='h-7 w-auto'
/>
</Link>
</div>
{/* Center cluster: search - absolutely positioned to center */}
<div className='-translate-x-1/2 absolute left-1/2 flex items-center justify-center'>
<SearchTrigger />
</div>
<div className='flex items-center gap-1'>
{/* Right cluster aligns with TOC edge */}
<div className='flex items-center gap-4'>
<Link
href='https://sim.ai'
target='_blank'
rel='noopener noreferrer'
className='rounded-xl px-3 py-2 font-normal text-[0.9375rem] text-foreground/60 leading-[1.4] transition-colors hover:bg-foreground/8 hover:text-foreground'
style={{
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
}}
>
Platform
</Link>
<LanguageDropdown />
<ThemeToggle />
</div>
</div>
{/* Divider — only spans content width */}
<div
className='border-b'
style={{
marginLeft: 'calc(var(--sidebar-offset) + 32px)',
marginRight: 'calc(var(--toc-offset) + 60px)',
borderColor: 'rgba(128, 128, 128, 0.1)',
}}
/>
{/* Bottom row: navigation tabs — border on row, tabs overlap it */}
<div
className='flex h-[40px] items-stretch gap-6 border-border/20 border-b'
style={{
paddingLeft: 'calc(var(--sidebar-offset) + 32px)',
}}
>
{NAV_TABS.map((tab) => {
const isActive = !tab.external && tab.match(pathname)
return (
<Link
key={tab.label}
href={tab.href}
{...(tab.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
className={cn(
'-mb-px relative flex items-center border-b text-[14px] tracking-[-0.01em] transition-colors',
isActive
? 'border-neutral-400 font-[550] text-neutral-800 dark:border-neutral-500 dark:text-neutral-200'
: 'border-transparent font-medium text-fd-muted-foreground hover:border-neutral-300 hover:text-neutral-600 dark:hover:border-neutral-600 dark:hover:text-neutral-400'
)}
>
{/* Invisible bold text reserves width to prevent layout shift */}
<span className='invisible font-[550]'>{tab.label}</span>
<span className='absolute'>{tab.label}</span>
</Link>
)
})}
</div>
</div>
</nav>
)

View File

@@ -1,13 +1,45 @@
'use client'
import { useState } from 'react'
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'
import { Check, Copy } from 'lucide-react'
export function LLMCopyButton({ content }: { content: string }) {
const [checked, onClick] = useCopyButton(() => navigator.clipboard.writeText(content))
const cache = new Map<string, string>()
export function LLMCopyButton({
markdownUrl,
}: {
/**
* A URL to fetch the raw Markdown/MDX content of page
*/
markdownUrl: string
}) {
const [isLoading, setLoading] = useState(false)
const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl)
if (cached) return navigator.clipboard.writeText(cached)
setLoading(true)
try {
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': fetch(markdownUrl).then(async (res) => {
const content = await res.text()
cache.set(markdownUrl, content)
return content
}),
}),
])
} finally {
setLoading(false)
}
})
return (
<button
disabled={isLoading}
onClick={onClick}
className='flex cursor-pointer items-center gap-1.5 rounded-lg border border-border/40 bg-background px-2.5 py-2 text-muted-foreground/60 text-sm leading-none transition-all hover:border-border hover:bg-accent/50 hover:text-muted-foreground'
aria-label={checked ? 'Copied to clipboard' : 'Copy page content'}

View File

@@ -25,8 +25,8 @@ export function StructuredData({
headline: title,
description: description,
url: url,
...(dateModified && { datePublished: dateModified }),
...(dateModified && { dateModified }),
datePublished: dateModified || new Date().toISOString(),
dateModified: dateModified || new Date().toISOString(),
author: {
'@type': 'Organization',
name: 'Sim Team',
@@ -74,7 +74,7 @@ export function StructuredData({
name: 'Sim Documentation',
url: baseUrl,
description:
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
'Comprehensive documentation for Sim visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.',
publisher: {
'@type': 'Organization',
name: 'Sim',
@@ -91,6 +91,12 @@ export function StructuredData({
inLanguage: ['en', 'es', 'fr', 'de', 'ja', 'zh'],
}
const faqStructuredData = title.toLowerCase().includes('faq') && {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: [],
}
const softwareStructuredData = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
@@ -98,7 +104,7 @@ export function StructuredData({
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any',
description:
'Sim is the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables, and docs.',
'Visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.',
url: baseUrl,
author: {
'@type': 'Organization',
@@ -109,13 +115,12 @@ export function StructuredData({
category: 'Developer Tools',
},
featureList: [
'AI agent creation',
'Agentic workflow orchestration',
'1,000+ integrations',
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
'Knowledge base creation',
'Table creation',
'Document creation',
'Visual workflow builder with drag-and-drop interface',
'AI agent creation and automation',
'80+ built-in integrations',
'Real-time team collaboration',
'Multiple deployment options',
'Custom integrations via MCP protocol',
],
}
@@ -146,6 +151,15 @@ export function StructuredData({
}}
/>
)}
{faqStructuredData && (
<Script
id='faq-structured-data'
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(faqStructuredData),
}}
/>
)}
{url === baseUrl && (
<Script
id='software-structured-data'

View File

@@ -1,87 +0,0 @@
'use client'
import { useState } from 'react'
import { cn, getAssetUrl } from '@/lib/utils'
import { Lightbox } from './lightbox'
interface ActionImageProps {
src: string
alt: string
enableLightbox?: boolean
}
interface ActionVideoProps {
src: string
alt: string
enableLightbox?: boolean
}
export function ActionImage({ src, alt, enableLightbox = true }: ActionImageProps) {
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const handleClick = () => {
if (enableLightbox) {
setIsLightboxOpen(true)
}
}
return (
<>
<img
src={src}
alt={alt}
onClick={handleClick}
className={cn(
'inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700',
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-90'
)}
/>
{enableLightbox && (
<Lightbox
isOpen={isLightboxOpen}
onClose={() => setIsLightboxOpen(false)}
src={src}
alt={alt}
type='image'
/>
)}
</>
)
}
export function ActionVideo({ src, alt, enableLightbox = true }: ActionVideoProps) {
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const resolvedSrc = getAssetUrl(src)
const handleClick = () => {
if (enableLightbox) {
setIsLightboxOpen(true)
}
}
return (
<>
<video
src={resolvedSrc}
autoPlay
loop
muted
playsInline
onClick={handleClick}
className={cn(
'inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700',
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-90'
)}
/>
{enableLightbox && (
<Lightbox
isOpen={isLightboxOpen}
onClose={() => setIsLightboxOpen(false)}
src={src}
alt={alt}
type='video'
/>
)}
</>
)
}

View File

@@ -1,371 +0,0 @@
'use client'
import { memo, useEffect, useState } from 'react'
/** Shared corner radius from Figma export for all decorative rects. */
const RX = '2.59574'
const ENTER_STAGGER = 0.06
const ENTER_DURATION = 0.3
const EXIT_STAGGER = 0.12
const EXIT_DURATION = 0.5
const INITIAL_HOLD = 3000
const HOLD_BETWEEN = 3000
const TRANSITION_PAUSE = 400
interface BlockRect {
opacity: number
width: string
height: string
fill: string
x?: string
y?: string
transform?: string
}
type AnimState = 'visible' | 'exiting' | 'hidden'
const RECTS = {
topRight: [
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '33.7252', fill: '#2ABBF8' },
{ opacity: 0.6, x: '0', y: '0', width: '85.3433', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 1, x: '0', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
{ opacity: 0.6, x: '34.2403', y: '0', width: '34.2403', height: '33.7252', fill: '#2ABBF8' },
{ opacity: 1, x: '34.2403', y: '0', width: '16.8626', height: '16.8626', fill: '#2ABBF8' },
{
opacity: 1,
x: '51.6188',
y: '16.8626',
width: '16.8626',
height: '16.8626',
fill: '#2ABBF8',
},
{ opacity: 1, x: '68.4812', y: '0', width: '54.6502', height: '16.8626', fill: '#00F701' },
{ opacity: 0.6, x: '106.268', y: '0', width: '34.2403', height: '33.7252', fill: '#00F701' },
{ opacity: 0.6, x: '106.268', y: '0', width: '51.103', height: '16.8626', fill: '#00F701' },
{
opacity: 1,
x: '123.6484',
y: '16.8626',
width: '16.8626',
height: '16.8626',
fill: '#00F701',
},
{ opacity: 0.6, x: '157.371', y: '0', width: '34.2403', height: '16.8626', fill: '#FFCC02' },
{ opacity: 1, x: '157.371', y: '0', width: '16.8626', height: '16.8626', fill: '#FFCC02' },
{ opacity: 0.6, x: '208.993', y: '0', width: '68.4805', height: '16.8626', fill: '#FA4EDF' },
{ opacity: 0.6, x: '209.137', y: '0', width: '16.8626', height: '33.7252', fill: '#FA4EDF' },
{ opacity: 0.6, x: '243.233', y: '0', width: '34.2403', height: '33.7252', fill: '#FA4EDF' },
{ opacity: 1, x: '243.233', y: '0', width: '16.8626', height: '16.8626', fill: '#FA4EDF' },
{ opacity: 0.6, x: '260.096', y: '0', width: '34.04', height: '16.8626', fill: '#FA4EDF' },
{
opacity: 1,
x: '260.611',
y: '16.8626',
width: '16.8626',
height: '16.8626',
fill: '#FA4EDF',
},
],
left: [
{
opacity: 0.6,
width: '34.240',
height: '33.725',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0 0)',
},
{
opacity: 0.6,
width: '16.8626',
height: '68.480',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.727 0)',
},
{
opacity: 1,
width: '16.8626',
height: '16.8626',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.727 17.378)',
},
{
opacity: 0.6,
width: '16.8626',
height: '33.986',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0 51.616)',
},
{
opacity: 0.6,
width: '16.8626',
height: '140.507',
fill: '#00F701',
transform: 'matrix(-1 0 0 1 33.986 85.335)',
},
{
opacity: 0.4,
x: '17.119',
y: '136.962',
width: '34.240',
height: '16.8626',
fill: '#FFCC02',
transform: 'rotate(-90 17.119 136.962)',
},
{
opacity: 1,
x: '17.119',
y: '136.962',
width: '16.8626',
height: '16.8626',
fill: '#FFCC02',
transform: 'rotate(-90 17.119 136.962)',
},
{
opacity: 0.5,
width: '34.240',
height: '33.725',
fill: '#00F701',
transform: 'matrix(0 1 1 0 0.257 153.825)',
},
{
opacity: 1,
width: '16.8626',
height: '16.8626',
fill: '#00F701',
transform: 'matrix(0 1 1 0 0.257 153.825)',
},
],
right: [
{
opacity: 0.6,
width: '16.8626',
height: '33.726',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0 0)',
},
{
opacity: 0.6,
width: '34.241',
height: '16.8626',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 16.891 0)',
},
{
opacity: 0.6,
width: '16.8626',
height: '68.482',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.739 16.888)',
},
{
opacity: 0.6,
width: '16.8626',
height: '33.726',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0 33.776)',
},
{
opacity: 1,
width: '16.8626',
height: '16.8626',
fill: '#FA4EDF',
transform: 'matrix(-1 0 0 1 33.739 34.272)',
},
{
opacity: 0.6,
width: '16.8626',
height: '33.726',
fill: '#FA4EDF',
transform: 'matrix(0 1 1 0 0.012 68.510)',
},
{
opacity: 0.6,
width: '16.8626',
height: '102.384',
fill: '#2ABBF8',
transform: 'matrix(-1 0 0 1 33.787 102.384)',
},
{
opacity: 0.4,
x: '17.131',
y: '153.859',
width: '34.241',
height: '16.8626',
fill: '#00F701',
transform: 'rotate(-90 17.131 153.859)',
},
{
opacity: 1,
x: '17.131',
y: '153.859',
width: '16.8626',
height: '16.8626',
fill: '#00F701',
transform: 'rotate(-90 17.131 153.859)',
},
],
} as const satisfies Record<string, readonly BlockRect[]>
type Position = keyof typeof RECTS
function enterTime(pos: Position): number {
return (RECTS[pos].length - 1) * ENTER_STAGGER + ENTER_DURATION
}
function exitTime(pos: Position): number {
return (RECTS[pos].length - 1) * EXIT_STAGGER + EXIT_DURATION
}
interface BlockGroupProps {
width: number
height: number
viewBox: string
rects: readonly BlockRect[]
animState: AnimState
globalOpacity: number
}
const BlockGroup = memo(function BlockGroup({
width,
height,
viewBox,
rects,
animState,
globalOpacity,
}: BlockGroupProps) {
const isVisible = animState === 'visible'
const isExiting = animState === 'exiting'
return (
<svg
width={width}
height={height}
viewBox={viewBox}
fill='none'
xmlns='http://www.w3.org/2000/svg'
className='h-auto w-full'
style={{ opacity: globalOpacity }}
>
{rects.map((r, i) => (
<rect
key={i}
x={r.x}
y={r.y}
width={r.width}
height={r.height}
rx={RX}
fill={r.fill}
transform={r.transform}
style={{
opacity: isVisible ? r.opacity : 0,
transition: `opacity ${isExiting ? EXIT_DURATION : ENTER_DURATION}s ease ${
isVisible ? i * ENTER_STAGGER : isExiting ? i * EXIT_STAGGER : 0
}s`,
}}
/>
))}
</svg>
)
})
function useGroupState(): [AnimState, (s: AnimState) => void] {
return useState<AnimState>('visible')
}
function useBlockCycle() {
const [topRight, setTopRight] = useGroupState()
const [left, setLeft] = useGroupState()
const [right, setRight] = useGroupState()
useEffect(() => {
if (typeof window !== 'undefined' && !window.matchMedia('(min-width: 1024px)').matches) return
const cancelled = { current: false }
const wait = (ms: number) => new Promise<void>((r) => setTimeout(r, ms))
async function exit(setter: (s: AnimState) => void, pos: Position, pauseAfter: number) {
if (cancelled.current) return
setter('exiting')
await wait(exitTime(pos) * 1000)
if (cancelled.current) return
setter('hidden')
await wait(pauseAfter)
}
async function enter(setter: (s: AnimState) => void, pos: Position, pauseAfter: number) {
if (cancelled.current) return
setter('visible')
await wait(enterTime(pos) * 1000 + pauseAfter)
}
const run = async () => {
await wait(INITIAL_HOLD)
while (!cancelled.current) {
await exit(setTopRight, 'topRight', TRANSITION_PAUSE)
await exit(setLeft, 'left', HOLD_BETWEEN)
await enter(setLeft, 'left', TRANSITION_PAUSE)
await enter(setTopRight, 'topRight', TRANSITION_PAUSE)
await exit(setRight, 'right', HOLD_BETWEEN)
await enter(setRight, 'right', HOLD_BETWEEN)
}
}
run()
return () => {
cancelled.current = true
}
}, [])
return { topRight, left, right } as const
}
/**
* Ambient animated block decorations for the docs layout.
* Adapts the landing page's colorful block patterns with slightly reduced
* opacity and the same staggered enter/exit animation cycle.
*/
export function AnimatedBlocks() {
const states = useBlockCycle()
return (
<div
className='pointer-events-none fixed inset-0 z-0 hidden overflow-hidden lg:block'
aria-hidden='true'
>
<div className='absolute top-[93px] right-0 w-[calc(140px+10.76vw)] max-w-[295px]'>
<BlockGroup
width={295}
height={34}
viewBox='0 0 295 34'
rects={RECTS.topRight}
animState={states.topRight}
globalOpacity={0.75}
/>
</div>
<div className='-translate-y-1/2 absolute top-[50%] left-0 w-[calc(16px+1.25vw)] max-w-[34px] scale-x-[-1]'>
<BlockGroup
width={34}
height={226}
viewBox='0 0 34 226.021'
rects={RECTS.left}
animState={states.left}
globalOpacity={0.75}
/>
</div>
<div className='-translate-y-1/2 absolute top-[50%] right-0 w-[calc(16px+1.25vw)] max-w-[34px]'>
<BlockGroup
width={34}
height={205}
viewBox='0 0 34 204.769'
rects={RECTS.right}
animState={states.right}
globalOpacity={0.75}
/>
</div>
</div>
)
}

View File

@@ -17,16 +17,23 @@ export function CodeBlock(props: React.ComponentProps<typeof FumadocsCodeBlock>)
return (
<FumadocsCodeBlock
{...props}
Actions={({ className }) => (
Actions={({ children, className }) => (
<div className={cn('empty:hidden', className)}>
{/* Custom copy button */}
<button
type='button'
aria-label={copied ? 'Copied Text' : 'Copy Text'}
onClick={(e) => {
const pre = (e.currentTarget as HTMLElement).closest('figure')?.querySelector('pre')
const pre = (e.currentTarget as HTMLElement)
.closest('.nd-codeblock')
?.querySelector('pre')
if (pre) handleCopy(pre.textContent || '')
}}
className='cursor-pointer rounded-md p-2 text-muted-foreground transition-colors hover:text-foreground'
className={cn(
'cursor-pointer rounded-md p-2 transition-all',
'border border-border bg-background/80 hover:bg-muted',
'backdrop-blur-sm'
)}
>
<span className='flex items-center justify-center'>
{copied ? (

View File

@@ -1,73 +0,0 @@
'use client'
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { Check } from 'lucide-react'
import { cn } from '@/lib/utils'
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] origin-[--radix-dropdown-menu-content-transform-origin] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator>
<Check className='h-4 w-4' />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
}

View File

@@ -1,47 +0,0 @@
'use client'
import { useState } from 'react'
import { ChevronRight } from 'lucide-react'
interface FAQItem {
question: string
answer: string
}
interface FAQProps {
items: FAQItem[]
title?: string
}
export function FAQ({ items, title = 'Common Questions' }: FAQProps) {
const [openIndex, setOpenIndex] = useState<number | null>(null)
return (
<div className='mt-12'>
<h2 className='mb-4 font-bold text-xl'>{title}</h2>
<div className='rounded-xl border border-border'>
{items.map((item, index) => (
<div key={index} className={index !== items.length - 1 ? 'border-border border-b' : ''}>
<button
type='button'
onClick={() => setOpenIndex(openIndex === index ? null : index)}
className='flex w-full cursor-pointer items-center gap-3 px-5 py-4 text-left font-medium text-[0.9375rem]'
>
<ChevronRight
className={`h-4 w-4 shrink-0 text-fd-muted-foreground transition-transform duration-200 ${
openIndex === index ? 'rotate-90' : ''
}`}
/>
{item.question}
</button>
{openIndex === index && (
<div className='px-5 pb-4 pl-12 text-[0.9375rem] text-fd-muted-foreground leading-relaxed'>
{item.answer}
</div>
)}
</div>
))}
</div>
</div>
)
}

View File

@@ -7,73 +7,44 @@ import {
A2AIcon,
AhrefsIcon,
AirtableIcon,
AirweaveIcon,
AlgoliaIcon,
AmplitudeIcon,
ApifyIcon,
ApolloIcon,
ArxivIcon,
AsanaIcon,
AshbyIcon,
AttioIcon,
BrainIcon,
BrandfetchIcon,
BrowserUseIcon,
CalComIcon,
CalendlyIcon,
CirclebackIcon,
ClayIcon,
ClerkIcon,
CloudflareIcon,
ConfluenceIcon,
CursorIcon,
DatabricksIcon,
DatadogIcon,
DevinIcon,
DiscordIcon,
DocumentIcon,
DropboxIcon,
DsPyIcon,
DubIcon,
DuckDuckGoIcon,
DynamoDBIcon,
ElasticsearchIcon,
ElevenLabsIcon,
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
EyeIcon,
FathomIcon,
FirecrawlIcon,
FirefliesIcon,
GammaIcon,
GithubIcon,
GitLabIcon,
GmailIcon,
GongIcon,
GoogleAdsIcon,
GoogleBigQueryIcon,
GoogleBooksIcon,
GoogleCalendarIcon,
GoogleContactsIcon,
GoogleDocsIcon,
GoogleDriveIcon,
GoogleFormsIcon,
GoogleGroupsIcon,
GoogleIcon,
GoogleMapsIcon,
GoogleMeetIcon,
GooglePagespeedIcon,
GoogleSheetsIcon,
GoogleSlidesIcon,
GoogleTasksIcon,
GoogleTranslateIcon,
GoogleVaultIcon,
GrafanaIcon,
GrainIcon,
GreenhouseIcon,
GreptileIcon,
HexIcon,
HubspotIcon,
HuggingFaceIcon,
HunterIOIcon,
@@ -84,18 +55,14 @@ import {
JiraIcon,
JiraServiceManagementIcon,
KalshiIcon,
LangsmithIcon,
LemlistIcon,
LinearIcon,
LinkedInIcon,
LinkupIcon,
LoopsIcon,
LumaIcon,
MailchimpIcon,
MailgunIcon,
MailServerIcon,
Mem0Icon,
MicrosoftDataverseIcon,
MicrosoftExcelIcon,
MicrosoftOneDriveIcon,
MicrosoftPlannerIcon,
@@ -106,12 +73,9 @@ import {
MySQLIcon,
Neo4jIcon,
NotionIcon,
ObsidianIcon,
OnePasswordIcon,
OpenAIIcon,
OutlookIcon,
PackageSearchIcon,
PagerDutyIcon,
ParallelIcon,
PerplexityIcon,
PineconeIcon,
@@ -119,14 +83,10 @@ import {
PolymarketIcon,
PostgresIcon,
PosthogIcon,
PulseIcon,
QdrantIcon,
RDSIcon,
RedditIcon,
RedisIcon,
ReductoIcon,
ResendIcon,
RevenueCatIcon,
S3Icon,
SalesforceIcon,
SearchIcon,
@@ -136,9 +96,9 @@ import {
ServiceNowIcon,
SftpIcon,
ShopifyIcon,
SimilarwebIcon,
SlackIcon,
SmtpIcon,
SpotifyIcon,
SQSIcon,
SshIcon,
STTIcon,
@@ -147,15 +107,12 @@ import {
SupabaseIcon,
TavilyIcon,
TelegramIcon,
TextractIcon,
TinybirdIcon,
TranslateIcon,
TrelloIcon,
TTSIcon,
TwilioIcon,
TypeformIcon,
UpstashIcon,
VercelIcon,
VideoIcon,
WealthboxIcon,
WebflowIcon,
@@ -175,71 +132,42 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
a2a: A2AIcon,
ahrefs: AhrefsIcon,
airtable: AirtableIcon,
airweave: AirweaveIcon,
algolia: AlgoliaIcon,
amplitude: AmplitudeIcon,
apify: ApifyIcon,
apollo: ApolloIcon,
arxiv: ArxivIcon,
asana: AsanaIcon,
ashby: AshbyIcon,
attio: AttioIcon,
brandfetch: BrandfetchIcon,
browser_use: BrowserUseIcon,
calcom: CalComIcon,
calendly: CalendlyIcon,
circleback: CirclebackIcon,
clay: ClayIcon,
clerk: ClerkIcon,
cloudflare: CloudflareIcon,
confluence_v2: ConfluenceIcon,
confluence: ConfluenceIcon,
cursor_v2: CursorIcon,
databricks: DatabricksIcon,
datadog: DatadogIcon,
devin: DevinIcon,
discord: DiscordIcon,
dropbox: DropboxIcon,
dspy: DsPyIcon,
dub: DubIcon,
duckduckgo: DuckDuckGoIcon,
dynamodb: DynamoDBIcon,
elasticsearch: ElasticsearchIcon,
elevenlabs: ElevenLabsIcon,
enrich: EnrichSoIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
fathom: FathomIcon,
file_v3: DocumentIcon,
file: DocumentIcon,
firecrawl: FirecrawlIcon,
fireflies_v2: FirefliesIcon,
gamma: GammaIcon,
fireflies: FirefliesIcon,
github_v2: GithubIcon,
gitlab: GitLabIcon,
gmail_v2: GmailIcon,
gong: GongIcon,
google_ads: GoogleAdsIcon,
google_bigquery: GoogleBigQueryIcon,
google_books: GoogleBooksIcon,
google_calendar_v2: GoogleCalendarIcon,
google_contacts: GoogleContactsIcon,
google_docs: GoogleDocsIcon,
google_drive: GoogleDriveIcon,
google_forms: GoogleFormsIcon,
google_groups: GoogleGroupsIcon,
google_maps: GoogleMapsIcon,
google_meet: GoogleMeetIcon,
google_pagespeed: GooglePagespeedIcon,
google_search: GoogleIcon,
google_sheets_v2: GoogleSheetsIcon,
google_slides_v2: GoogleSlidesIcon,
google_tasks: GoogleTasksIcon,
google_translate: GoogleTranslateIcon,
google_sheets: GoogleSheetsIcon,
google_slides: GoogleSlidesIcon,
google_vault: GoogleVaultIcon,
grafana: GrafanaIcon,
grain: GrainIcon,
greenhouse: GreenhouseIcon,
greptile: GreptileIcon,
hex: HexIcon,
hubspot: HubspotIcon,
huggingface: HuggingFaceIcon,
hunter: HunterIOIcon,
@@ -250,34 +178,27 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
jina: JinaAIIcon,
jira: JiraIcon,
jira_service_management: JiraServiceManagementIcon,
kalshi_v2: KalshiIcon,
kalshi: KalshiIcon,
knowledge: PackageSearchIcon,
langsmith: LangsmithIcon,
lemlist: LemlistIcon,
linear: LinearIcon,
linkedin: LinkedInIcon,
linkup: LinkupIcon,
loops: LoopsIcon,
luma: LumaIcon,
mailchimp: MailchimpIcon,
mailgun: MailgunIcon,
mem0: Mem0Icon,
memory: BrainIcon,
microsoft_dataverse: MicrosoftDataverseIcon,
microsoft_excel_v2: MicrosoftExcelIcon,
microsoft_excel: MicrosoftExcelIcon,
microsoft_planner: MicrosoftPlannerIcon,
microsoft_teams: MicrosoftTeamsIcon,
mistral_parse_v3: MistralIcon,
mistral_parse: MistralIcon,
mongodb: MongoDBIcon,
mysql: MySQLIcon,
neo4j: Neo4jIcon,
notion_v2: NotionIcon,
obsidian: ObsidianIcon,
onedrive: MicrosoftOneDriveIcon,
onepassword: OnePasswordIcon,
openai: OpenAIIcon,
outlook: OutlookIcon,
pagerduty: PagerDutyIcon,
parallel_ai: ParallelIcon,
perplexity: PerplexityIcon,
pinecone: PineconeIcon,
@@ -285,14 +206,10 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
polymarket: PolymarketIcon,
postgresql: PostgresIcon,
posthog: PosthogIcon,
pulse_v2: PulseIcon,
qdrant: QdrantIcon,
rds: RDSIcon,
reddit: RedditIcon,
redis: RedisIcon,
reducto_v2: ReductoIcon,
resend: ResendIcon,
revenuecat: RevenueCatIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
search: SearchIcon,
@@ -303,18 +220,18 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
sftp: SftpIcon,
sharepoint: MicrosoftSharepointIcon,
shopify: ShopifyIcon,
similarweb: SimilarwebIcon,
slack: SlackIcon,
smtp: SmtpIcon,
spotify: SpotifyIcon,
sqs: SQSIcon,
ssh: SshIcon,
stagehand: StagehandIcon,
stripe: StripeIcon,
stt_v2: STTIcon,
stt: STTIcon,
supabase: SupabaseIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
textract_v2: TextractIcon,
thinking: BrainIcon,
tinybird: TinybirdIcon,
translate: TranslateIcon,
trello: TrelloIcon,
@@ -322,10 +239,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
twilio_sms: TwilioIcon,
twilio_voice: TwilioIcon,
typeform: TypeformIcon,
upstash: UpstashIcon,
vercel: VercelIcon,
video_generator_v2: VideoIcon,
vision_v2: EyeIcon,
video_generator: VideoIcon,
vision: EyeIcon,
wealthbox: WealthboxIcon,
webflow: WebflowIcon,
whatsapp: WhatsAppIcon,

View File

@@ -30,7 +30,7 @@ export function Image({
<NextImage
className={cn(
'overflow-hidden rounded-xl border border-border object-cover shadow-sm',
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-95',
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-90',
className
)}
alt={alt}

View File

@@ -1,9 +1,8 @@
'use client'
import { useEffect, useState } from 'react'
import { Check, ChevronDown } from 'lucide-react'
import { Check, ChevronRight } from 'lucide-react'
import { useParams, usePathname, useRouter } from 'next/navigation'
import { cn } from '@/lib/utils'
const languages = {
en: { name: 'English', flag: '🇺🇸' },
@@ -16,7 +15,6 @@ const languages = {
export function LanguageDropdown() {
const [isOpen, setIsOpen] = useState(false)
const [hoveredIndex, setHoveredIndex] = useState<number>(-1)
const pathname = usePathname()
const params = useParams()
const router = useRouter()
@@ -73,15 +71,6 @@ export function LanguageDropdown() {
return () => window.removeEventListener('keydown', onKey)
}, [isOpen])
// Reset hovered index when popover closes
useEffect(() => {
if (!isOpen) {
setHoveredIndex(-1)
}
}, [isOpen])
const languageEntries = Object.entries(languages)
return (
<div className='relative'>
<button
@@ -93,14 +82,14 @@ export function LanguageDropdown() {
aria-haspopup='listbox'
aria-expanded={isOpen}
aria-controls='language-menu'
className='flex cursor-pointer items-center gap-1.5 rounded-[6px] px-3 py-2 font-normal text-[0.9375rem] text-foreground/60 leading-[1.4] transition-colors hover:bg-foreground/8 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring'
className='flex cursor-pointer items-center gap-1.5 rounded-xl px-3 py-2 font-normal text-[0.9375rem] text-foreground/60 leading-[1.4] transition-colors hover:bg-foreground/8 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring'
style={{
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
}}
>
<span>{languages[currentLang as keyof typeof languages]?.name}</span>
<ChevronDown className={cn('h-3.5 w-3.5 transition-transform', isOpen && 'rotate-180')} />
<ChevronRight className='h-3.5 w-3.5' />
</button>
{isOpen && (
@@ -109,37 +98,29 @@ export function LanguageDropdown() {
<div
id='language-menu'
role='listbox'
className='absolute top-full right-0 z-[1001] mt-2 max-h-[400px] min-w-[160px] overflow-auto rounded-[6px] bg-white px-[6px] py-[6px] shadow-lg dark:bg-neutral-900'
className='absolute top-full right-0 z-[1001] mt-1 max-h-[75vh] w-56 overflow-auto rounded-xl border border-border/50 bg-white shadow-2xl md:w-44 md:bg-background/95 md:backdrop-blur-md dark:bg-neutral-950 md:dark:bg-background/95'
>
{languageEntries.map(([code, lang], index) => {
const isSelected = currentLang === code
const isHovered = hoveredIndex === index
return (
<button
key={code}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleLanguageChange(code)
}}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(-1)}
role='option'
aria-selected={isSelected}
className={cn(
'flex h-[26px] w-full min-w-0 cursor-pointer items-center gap-[8px] rounded-[6px] px-[6px] text-[13px] transition-colors',
'text-neutral-700 dark:text-neutral-200',
isHovered && 'bg-neutral-100 dark:bg-neutral-800',
'focus:outline-none'
)}
>
<span className='text-[13px]'>{lang.flag}</span>
<span className='flex-1 text-left leading-none'>{lang.name}</span>
{isSelected && <Check className='ml-auto h-3.5 w-3.5' />}
</button>
)
})}
{Object.entries(languages).map(([code, lang]) => (
<button
key={code}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleLanguageChange(code)
}}
role='option'
aria-selected={currentLang === code}
className={`flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-base transition-colors first:rounded-t-xl last:rounded-b-xl hover:bg-muted/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring md:gap-2 md:px-2.5 md:py-2 md:text-sm ${
currentLang === code ? 'bg-muted/60 font-medium text-primary' : 'text-foreground'
}`}
>
<span className='text-base md:text-sm'>{lang.flag}</span>
<span className='leading-none'>{lang.name}</span>
{currentLang === code && (
<Check className='ml-auto h-4 w-4 text-primary md:h-3.5 md:w-3.5' />
)}
</button>
))}
</div>
</>
)}

View File

@@ -55,9 +55,8 @@ export function Lightbox({ isOpen, onClose, src, alt, type }: LightboxProps) {
<img
src={src}
alt={alt}
className='max-h-[75vh] max-w-[75vw] cursor-pointer rounded-xl object-contain'
className='max-h-[calc(100vh-6rem)] max-w-[calc(100vw-6rem)] rounded-xl object-contain'
loading='lazy'
onClick={onClose}
/>
) : (
<video
@@ -66,8 +65,7 @@ export function Lightbox({ isOpen, onClose, src, alt, type }: LightboxProps) {
loop
muted
playsInline
className='max-h-[75vh] max-w-[75vw] cursor-pointer rounded-xl outline-none focus:outline-none'
onClick={onClose}
className='max-h-[calc(100vh-6rem)] max-w-[calc(100vw-6rem)] rounded-xl outline-none focus:outline-none'
/>
)}
</div>

View File

@@ -1,169 +0,0 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
interface ResponseSectionProps {
children: React.ReactNode
}
export function ResponseSection({ children }: ResponseSectionProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [statusCodes, setStatusCodes] = useState<string[]>([])
const [selectedCode, setSelectedCode] = useState<string>('')
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
function getAccordionItems() {
const root = containerRef.current?.querySelector('[data-orientation="vertical"]')
if (!root) return []
return Array.from(root.children).filter(
(el) => el.getAttribute('data-state') !== null
) as HTMLElement[]
}
function showStatusCode(code: string) {
const items = getAccordionItems()
for (const item of items) {
const triggerBtn = item.querySelector('h3 button') as HTMLButtonElement | null
const text = triggerBtn?.textContent?.trim() ?? ''
const itemCode = text.match(/^\d{3}/)?.[0]
if (itemCode === code) {
item.style.display = ''
if (item.getAttribute('data-state') === 'closed' && triggerBtn) {
triggerBtn.click()
}
} else {
item.style.display = 'none'
if (item.getAttribute('data-state') === 'open' && triggerBtn) {
triggerBtn.click()
}
}
}
}
/**
* Detect when the fumadocs accordion children mount via MutationObserver,
* then extract status codes and show the first one.
* Replaces the previous approach that used `children` as a dependency
* (which triggered on every render since children is a new object each time).
*/
useEffect(() => {
const container = containerRef.current
if (!container) return
const initialize = () => {
const items = getAccordionItems()
if (items.length === 0) return false
const codes: string[] = []
const seen = new Set<string>()
for (const item of items) {
const triggerBtn = item.querySelector('h3 button')
if (triggerBtn) {
const text = triggerBtn.textContent?.trim() ?? ''
const code = text.match(/^\d{3}/)?.[0]
if (code && !seen.has(code)) {
seen.add(code)
codes.push(code)
}
}
}
if (codes.length > 0) {
setStatusCodes(codes)
setSelectedCode(codes[0])
showStatusCode(codes[0])
return true
}
return false
}
if (initialize()) return
const observer = new MutationObserver(() => {
if (initialize()) {
observer.disconnect()
}
})
observer.observe(container, { childList: true, subtree: true })
return () => observer.disconnect()
}, []) // eslint-disable-line react-hooks/exhaustive-deps
function handleSelectCode(code: string) {
setSelectedCode(code)
setIsOpen(false)
showStatusCode(code)
}
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
return (
<div ref={containerRef} className='response-section-wrapper'>
{statusCodes.length > 0 && (
<div className='response-section-header'>
<h2 className='response-section-title'>Response</h2>
<div className='response-section-meta'>
<div ref={dropdownRef} className='response-section-dropdown-wrapper'>
<button
type='button'
className='response-section-dropdown-trigger'
onClick={() => setIsOpen(!isOpen)}
>
<span>{selectedCode}</span>
<ChevronDown
className={cn(
'response-section-chevron',
isOpen && 'response-section-chevron-open'
)}
/>
</button>
{isOpen && (
<div className='response-section-dropdown-menu'>
{statusCodes.map((code) => (
<button
key={code}
type='button'
className={cn(
'response-section-dropdown-item',
code === selectedCode && 'response-section-dropdown-item-selected'
)}
onClick={() => handleSelectCode(code)}
>
<span>{code}</span>
{code === selectedCode && (
<svg
className='response-section-check'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
>
<polyline points='20 6 9 17 4 12' />
</svg>
)}
</button>
))}
</div>
)}
</div>
<span className='response-section-content-type'>application/json</span>
</div>
</div>
)}
<div className='response-section-content'>{children}</div>
</div>
)
}

View File

@@ -15,14 +15,23 @@ export function SearchTrigger() {
return (
<button
type='button'
className='flex h-9 w-[360px] cursor-pointer items-center gap-2 rounded-lg border border-border/50 bg-fd-muted/50 px-3 text-[13px] text-fd-muted-foreground transition-colors hover:border-border hover:text-fd-foreground'
className='flex h-10 w-[460px] cursor-pointer items-center gap-2 rounded-xl border border-border/50 px-3 py-2 text-sm backdrop-blur-xl transition-colors hover:border-border'
style={{
backgroundColor: 'hsla(0, 0%, 5%, 0.85)',
backdropFilter: 'blur(33px) saturate(180%)',
WebkitBackdropFilter: 'blur(33px) saturate(180%)',
color: 'rgba(255, 255, 255, 0.6)',
}}
onClick={handleClick}
>
<Search className='h-3.5 w-3.5' />
<Search className='h-4 w-4' />
<span>Search...</span>
<kbd className='ml-auto flex items-center font-medium'>
<span className='text-[15px]'></span>
<span className='text-[12px]'>K</span>
<kbd
className='ml-auto flex items-center gap-0.5 font-medium'
style={{ color: 'rgba(255, 255, 255, 0.6)' }}
>
<span style={{ fontSize: '15px', lineHeight: '1' }}></span>
<span style={{ fontSize: '13px', lineHeight: '1' }}>K</span>
</kbd>
</button>
)

View File

@@ -1,108 +0,0 @@
'use client'
import { cn } from '@/lib/utils'
interface SimLogoProps {
className?: string
}
/**
* Sim logo with icon and text.
* The icon stays green (#33C482), text adapts to light/dark mode.
*/
export function SimLogo({ className }: SimLogoProps) {
return (
<svg
viewBox='720 440 320 320'
fill='none'
xmlns='http://www.w3.org/2000/svg'
className={cn('h-7 w-auto', className)}
aria-label='Sim'
>
{/* Green icon - top left shape with cutout */}
<path
fillRule='evenodd'
clipRule='evenodd'
d='M875.791 577.171C875.791 581.922 873.911 586.483 870.576 589.842L870.098 590.323C866.764 593.692 862.234 595.575 857.517 595.575H750.806C740.978 595.575 733 603.6 733 613.498V728.902C733 738.799 740.978 746.826 750.806 746.826H865.382C875.209 746.826 883.177 738.799 883.177 728.902V620.853C883.177 616.448 884.912 612.222 888.008 609.104C891.093 605.997 895.29 604.249 899.664 604.249H1008.16C1017.99 604.249 1025.96 596.224 1025.96 586.327V470.923C1025.96 461.025 1017.99 453 1008.16 453H893.586C883.759 453 875.791 461.025 875.791 470.923V577.171ZM910.562 477.566H991.178C996.922 477.566 1001.57 482.254 1001.57 488.029V569.22C1001.57 574.995 996.922 579.683 991.178 579.683H910.562C904.828 579.683 900.173 574.995 900.173 569.22V488.029C900.173 482.254 904.828 477.566 910.562 477.566Z'
fill='#33C482'
/>
{/* Green icon - bottom right square */}
<path
d='M1008.3 624.59H923.113C912.786 624.59 904.414 633.022 904.414 643.423V728.171C904.414 738.572 912.786 747.004 923.113 747.004H1008.3C1018.63 747.004 1027 738.572 1027 728.171V643.423C1027 633.022 1018.63 624.59 1008.3 624.59Z'
fill='#33C482'
/>
{/* Gradient overlay on bottom right square */}
<path
d='M1008.3 624.199H923.113C912.786 624.199 904.414 632.631 904.414 643.033V727.78C904.414 738.181 912.786 746.612 923.113 746.612H1008.3C1018.63 746.612 1027 738.181 1027 727.78V643.033C1027 632.631 1018.63 624.199 1008.3 624.199Z'
fill='url(#sim-logo-gradient)'
fillOpacity='0.2'
/>
<defs>
<linearGradient
id='sim-logo-gradient'
x1='904.414'
y1='624.199'
x2='978.836'
y2='698.447'
gradientUnits='userSpaceOnUse'
>
<stop />
<stop offset='1' stopOpacity='0' />
</linearGradient>
</defs>
</svg>
)
}
/**
* Full Sim logo with icon and "Sim" text.
* The icon stays green (#33C482), text adapts to light/dark mode.
*/
export function SimLogoFull({ className }: SimLogoProps) {
return (
<svg
viewBox='720 440 1020 320'
fill='none'
xmlns='http://www.w3.org/2000/svg'
className={cn('h-7 w-auto', className)}
aria-label='Sim'
>
{/* Green icon - top left shape with cutout */}
<path
fillRule='evenodd'
clipRule='evenodd'
d='M875.791 577.171C875.791 581.922 873.911 586.483 870.576 589.842L870.098 590.323C866.764 593.692 862.234 595.575 857.517 595.575H750.806C740.978 595.575 733 603.6 733 613.498V728.902C733 738.799 740.978 746.826 750.806 746.826H865.382C875.209 746.826 883.177 738.799 883.177 728.902V620.853C883.177 616.448 884.912 612.222 888.008 609.104C891.093 605.997 895.29 604.249 899.664 604.249H1008.16C1017.99 604.249 1025.96 596.224 1025.96 586.327V470.923C1025.96 461.025 1017.99 453 1008.16 453H893.586C883.759 453 875.791 461.025 875.791 470.923V577.171ZM910.562 477.566H991.178C996.922 477.566 1001.57 482.254 1001.57 488.029V569.22C1001.57 574.995 996.922 579.683 991.178 579.683H910.562C904.828 579.683 900.173 574.995 900.173 569.22V488.029C900.173 482.254 904.828 477.566 910.562 477.566Z'
fill='#33C482'
/>
{/* Green icon - bottom right square */}
<path
d='M1008.3 624.59H923.113C912.786 624.59 904.414 633.022 904.414 643.423V728.171C904.414 738.572 912.786 747.004 923.113 747.004H1008.3C1018.63 747.004 1027 738.572 1027 728.171V643.423C1027 633.022 1018.63 624.59 1008.3 624.59Z'
fill='#33C482'
/>
{/* Gradient overlay on bottom right square */}
<path
d='M1008.3 624.199H923.113C912.786 624.199 904.414 632.631 904.414 643.033V727.78C904.414 738.181 912.786 746.612 923.113 746.612H1008.3C1018.63 746.612 1027 738.181 1027 727.78V643.033C1027 632.631 1018.63 624.199 1008.3 624.199Z'
fill='url(#sim-logo-full-gradient)'
fillOpacity='0.2'
/>
{/* "Sim" text - adapts to light/dark mode via currentColor */}
<path
d='M1210.54 515.657C1226.65 515.657 1240.59 518.51 1252.31 524.257H1252.31C1264.3 529.995 1273.63 538.014 1280.26 548.319H1280.26C1287.19 558.635 1290.78 570.899 1291.08 585.068L1291.1 586.089H1249.11L1249.09 585.115C1248.8 574.003 1245.18 565.493 1238.32 559.451C1231.45 553.399 1221.79 550.308 1209.21 550.308C1196.3 550.308 1186.48 553.113 1179.61 558.588C1172.76 564.046 1169.33 571.499 1169.33 581.063C1169.33 588.092 1171.88 593.978 1177.01 598.783C1182.17 603.618 1189.99 607.399 1200.56 610.061H1200.56L1238.77 619.451C1257.24 623.65 1271.21 630.571 1280.57 640.293L1281.01 640.739C1290.13 650.171 1294.64 662.97 1294.64 679.016C1294.64 692.923 1290.88 705.205 1283.34 715.822L1283.33 715.834C1275.81 726.134 1265.44 734.14 1252.26 739.866L1252.25 739.871C1239.36 745.302 1224.12 748 1206.54 748C1180.9 748 1160.36 741.696 1145.02 728.984C1129.67 716.258 1122 699.269 1122 678.121V677.121H1163.99V678.121C1163.99 688.869 1167.87 697.367 1175.61 703.722L1176.34 704.284C1184.04 709.997 1194.37 712.902 1207.43 712.902C1222.13 712.902 1233.3 710.087 1241.07 704.588C1248.8 698.812 1252.64 691.21 1252.64 681.699C1252.64 674.769 1250.5 669.057 1246.25 664.49L1246.23 664.478L1246.22 664.464C1242.28 659.929 1234.83 656.119 1223.64 653.152L1185.43 644.208L1185.42 644.204C1166.05 639.407 1151.49 632.035 1141.83 622.012L1141.83 622.006L1141.82 622C1132.43 611.94 1127.78 598.707 1127.78 582.405C1127.78 568.81 1131.23 556.976 1138.17 546.949L1138.18 546.941L1138.19 546.933C1145.41 536.936 1155.18 529.225 1167.48 523.793L1167.48 523.79C1180.07 518.36 1194.43 515.657 1210.54 515.657ZM1323.39 521.979C1331.68 525.008 1337.55 526.482 1343.51 526.482C1349.48 526.482 1355.64 525.005 1364.49 521.973L1365.82 521.52V742.633H1322.05V521.489L1323.39 521.979ZM1642.01 515.657C1667.11 515.657 1686.94 523.031 1701.39 537.876C1715.83 552.716 1723 572.968 1723 598.507V742.633H1680.12V608.794C1680.12 591.666 1675.72 578.681 1667.07 569.681L1667.06 569.669L1667.04 569.656C1658.67 560.359 1647.26 555.675 1632.68 555.675C1622.47 555.675 1613.47 558.022 1605.64 562.69L1605.63 562.696C1598.11 567.064 1592.17 573.475 1587.8 581.968C1583.44 590.448 1581.25 600.424 1581.25 611.925V742.633H1537.92V608.347C1537.92 591.208 1533.67 578.376 1525.31 569.68L1525.31 569.674L1525.3 569.668C1516.93 560.664 1505.52 556.122 1490.93 556.122C1480.72 556.122 1471.72 558.469 1463.89 563.138L1463.88 563.144C1456.36 567.511 1450.41 573.922 1446.05 582.415L1446.05 582.422L1446.04 582.428C1441.69 590.602 1439.5 600.423 1439.5 611.925V742.633H1395.72V521.919H1435.05V554.803C1439.92 544.379 1447.91 535.465 1458.37 528.356C1470.71 519.875 1485.58 515.657 1502.93 515.657C1522.37 515.657 1538.61 520.931 1551.55 531.538C1560.38 538.771 1567.1 547.628 1571.72 558.091C1576.05 547.619 1582.83 538.757 1592.07 531.524C1605.61 520.93 1622.28 515.657 1642.01 515.657ZM1343.49 452C1351.45 452 1358.23 454.786 1363.75 460.346C1369.27 465.905 1372.04 472.721 1372.04 480.73C1372.04 488.452 1369.27 495.254 1363.77 501.096L1363.76 501.105L1363.75 501.115C1358.23 506.675 1351.45 509.461 1343.49 509.461C1335.81 509.461 1329.05 506.669 1323.25 501.134L1323.23 501.115L1323.21 501.096C1317.71 495.254 1314.94 488.452 1314.94 480.73C1314.94 472.721 1317.7 465.905 1323.23 460.346L1323.24 460.337L1323.25 460.327C1329.05 454.792 1335.81 452 1343.49 452Z'
className='fill-neutral-900 dark:fill-white'
/>
<defs>
<linearGradient
id='sim-logo-full-gradient'
x1='904.414'
y1='624.199'
x2='978.836'
y2='698.447'
gradientUnits='userSpaceOnUse'
>
<stop />
<stop offset='1' stopOpacity='0' />
</linearGradient>
</defs>
</svg>
)
}

View File

@@ -38,7 +38,7 @@ export function Video({
loop={loop}
muted={muted}
playsInline={playsInline}
className={`${className} ${enableLightbox ? 'cursor-pointer transition-opacity hover:opacity-95' : ''}`}
className={`${className} ${enableLightbox ? 'cursor-pointer transition-opacity hover:opacity-90' : ''}`}
src={getAssetUrl(src)}
onClick={handleVideoClick}
/>

View File

@@ -1,94 +0,0 @@
---
title: Authentication
description: API key types, generation, and how to authenticate requests
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
To access the Sim API, you need an API key. Sim supports two types of API keys — **personal keys** and **workspace keys** — each with different billing and access behaviors.
## Key Types
| | **Personal Keys** | **Workspace Keys** |
| --- | --- | --- |
| **Billed to** | Your individual account | Workspace owner |
| **Scope** | Across workspaces you have access to | Shared across the workspace |
| **Managed by** | Each user individually | Workspace admins |
| **Permissions** | Must be enabled at workspace level | Require admin permissions |
<Callout type="info">
Workspace admins can disable personal API key usage for their workspace. If disabled, only workspace keys can be used.
</Callout>
## Generating API Keys
To generate a key, open the Sim dashboard and navigate to **Settings**, then go to **Sim Keys** and click **Create**.
<Callout type="warn">
API keys are only shown once when generated. Store your key securely — you will not be able to view it again.
</Callout>
## Using API Keys
Pass your API key in the `X-API-Key` header with every request:
<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">
```bash
curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"inputs": {}}'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
'https://www.sim.ai/api/workflows/{workflowId}/execute',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.SIM_API_KEY!,
},
body: JSON.stringify({ inputs: {} }),
}
)
```
</Tab>
<Tab value="Python">
```python
import requests
response = requests.post(
"https://www.sim.ai/api/workflows/{workflowId}/execute",
headers={
"Content-Type": "application/json",
"X-API-Key": os.environ["SIM_API_KEY"],
},
json={"inputs": {}},
)
```
</Tab>
</Tabs>
## Where Keys Are Used
API keys authenticate access to:
- **Workflow execution** — run deployed workflows via the API
- **Logs API** — query workflow execution logs and metrics
- **MCP servers** — authenticate connections to deployed MCP servers
- **SDKs** — the [Python](/api-reference/python) and [TypeScript](/api-reference/typescript) SDKs use API keys for all operations
## Security
- Keys use the `sk-sim-` prefix and are encrypted at rest
- Keys can be revoked at any time from the dashboard
- Use environment variables to store keys — never hardcode them in source code
- For browser-based applications, use a backend proxy to avoid exposing keys to the client
<Callout type="warn">
Never expose your API key in client-side code. Use a server-side proxy to make authenticated requests on behalf of your frontend.
</Callout>

View File

@@ -1,210 +0,0 @@
---
title: Getting Started
description: Base URL, first API call, response format, error handling, and pagination
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Step, Steps } from 'fumadocs-ui/components/steps'
## Base URL
All API requests are made to:
```
https://www.sim.ai
```
## Quick Start
<Steps>
<Step>
### Get your API key
Go to the Sim dashboard and navigate to **Settings → Sim Keys**, then click **Create**. See [Authentication](/api-reference/authentication) for details on key types.
</Step>
<Step>
### Find your workflow ID
Open a workflow in the Sim editor. The workflow ID is in the URL:
```
https://www.sim.ai/workspace/{workspaceId}/w/{workflowId}
```
You can also use the [List Workflows](/api-reference/workflows/listWorkflows) endpoint to get all workflow IDs in a workspace.
</Step>
<Step>
### Deploy your workflow
A workflow must be deployed before it can be executed via the API. Click the **Deploy** button in the editor toolbar.
</Step>
<Step>
### Make your first request
<Tabs items={['curl', 'TypeScript', 'Python']}>
<Tab value="curl">
```bash
curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"inputs": {}}'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch(
`https://www.sim.ai/api/workflows/${workflowId}/execute`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.SIM_API_KEY!,
},
body: JSON.stringify({ inputs: {} }),
}
)
const data = await response.json()
console.log(data.output)
```
</Tab>
<Tab value="Python">
```python
import requests
import os
response = requests.post(
f"https://www.sim.ai/api/workflows/{workflow_id}/execute",
headers={
"Content-Type": "application/json",
"X-API-Key": os.environ["SIM_API_KEY"],
},
json={"inputs": {}},
)
data = response.json()
print(data["output"])
```
</Tab>
</Tabs>
</Step>
</Steps>
## Sync vs Async Execution
By default, workflow executions are **synchronous** — the API blocks until the workflow completes and returns the result directly.
For long-running workflows, use **asynchronous execution** by passing `async: true`:
```bash
curl -X POST https://www.sim.ai/api/workflows/{workflowId}/execute \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"inputs": {}, "async": true}'
```
This returns immediately with a `taskId`:
```json
{
"success": true,
"taskId": "job_abc123",
"status": "queued"
}
```
Poll the [Get Job Status](/api-reference/workflows/getJobStatus) endpoint until the status is `completed` or `failed`:
```bash
curl https://www.sim.ai/api/jobs/{taskId} \
-H "X-API-Key: YOUR_API_KEY"
```
<Callout type="info">
Job status transitions follow: `queued` → `processing` → `completed` or `failed`. The `output` field is only present when status is `completed`.
</Callout>
## Response Format
Successful responses include an `output` object with your workflow results and a `limits` object with your current rate limit and usage status:
```json
{
"success": true,
"output": {
"result": "Hello, world!"
},
"limits": {
"workflowExecutionRateLimit": {
"sync": {
"requestsPerMinute": 60,
"maxBurst": 10,
"remaining": 59,
"resetAt": "2025-01-01T00:01:00Z"
},
"async": {
"requestsPerMinute": 30,
"maxBurst": 5,
"remaining": 30,
"resetAt": "2025-01-01T00:01:00Z"
}
},
"usage": {
"currentPeriodCost": 1.25,
"limit": 50.00,
"plan": "pro",
"isExceeded": false
}
}
}
```
## Error Handling
The API uses standard HTTP status codes. Error responses include a human-readable `error` message:
```json
{
"error": "Workflow not found"
}
```
| Status | Meaning | What to do |
| --- | --- | --- |
| `400` | Invalid request parameters | Check the `details` array for specific field errors |
| `401` | Missing or invalid API key | Verify your `X-API-Key` header |
| `403` | Access denied | Check you have permission for this resource |
| `404` | Resource not found | Verify the ID exists and belongs to your workspace |
| `429` | Rate limit exceeded | Wait for the duration in the `Retry-After` header |
<Callout type="info">
Use the [Get Usage Limits](/api-reference/usage/getUsageLimits) endpoint to check your current rate limit status and billing usage at any time.
</Callout>
## Rate Limits
Rate limits depend on your subscription plan and apply separately to synchronous and asynchronous executions. Every execution response includes a `limits` object showing your current rate limit status.
When rate limited, the API returns a `429` response with a `Retry-After` header indicating how many seconds to wait before retrying.
## Pagination
List endpoints (workflows, logs, audit logs) use **cursor-based pagination**:
```bash
# First page
curl "https://www.sim.ai/api/v1/logs?limit=20" \
-H "X-API-Key: YOUR_API_KEY"
# Next page — use the nextCursor from the previous response
curl "https://www.sim.ai/api/v1/logs?limit=20&cursor=abc123" \
-H "X-API-Key: YOUR_API_KEY"
```
The response includes a `nextCursor` field. When `nextCursor` is absent or `null`, you have reached the last page.

View File

@@ -1,18 +0,0 @@
{
"title": "API Reference",
"root": true,
"pages": [
"getting-started",
"authentication",
"---SDKs---",
"python",
"typescript",
"---Endpoints---",
"(generated)/workflows",
"(generated)/logs",
"(generated)/usage",
"(generated)/audit-logs",
"(generated)/tables",
"(generated)/files"
]
}

View File

@@ -1,766 +0,0 @@
---
title: Python
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Card, Cards } from 'fumadocs-ui/components/card'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Das offizielle Python SDK für Sim ermöglicht es Ihnen, Workflows programmatisch aus Ihren Python-Anwendungen heraus mit dem offiziellen Python SDK auszuführen.
<Callout type="info">
Das Python SDK unterstützt Python 3.8+ mit Unterstützung für asynchrone Ausführung, automatischer Ratenbegrenzung mit exponentiellem Backoff und Nutzungsverfolgung.
</Callout>
## Installation
Installieren Sie das SDK mit pip:
```bash
pip install simstudio-sdk
```
## Schnellstart
Hier ist ein einfaches Beispiel für den Einstieg:
```python
from simstudio import SimStudioClient
# Initialize the client
client = SimStudioClient(
api_key="your-api-key-here",
base_url="https://sim.ai" # optional, defaults to https://sim.ai
)
# Execute a workflow
try:
result = client.execute_workflow("workflow-id")
print("Workflow executed successfully:", result)
except Exception as error:
print("Workflow execution failed:", error)
```
## API-Referenz
### SimStudioClient
#### Konstruktor
```python
SimStudioClient(api_key: str, base_url: str = "https://sim.ai")
```
**Parameter:**
- `api_key` (str): Ihr Sim API-Schlüssel
- `base_url` (str, optional): Basis-URL für die Sim API
#### Methoden
##### execute_workflow()
Führt einen Workflow mit optionalen Eingabedaten aus.
```python
result = client.execute_workflow(
"workflow-id",
input_data={"message": "Hello, world!"},
timeout=30.0 # 30 seconds
)
```
**Parameter:**
- `workflow_id` (str): Die ID des auszuführenden Workflows
- `input_data` (dict, optional): Eingabedaten, die an den Workflow übergeben werden
- `timeout` (float, optional): Timeout in Sekunden (Standard: 30.0)
- `stream` (bool, optional): Streaming-Antworten aktivieren (Standard: False)
- `selected_outputs` (list[str], optional): Block-Ausgaben zum Streamen im Format `blockName.attribute` (z. B. `["agent1.content"]`)
- `async_execution` (bool, optional): Asynchron ausführen (Standard: False)
**Rückgabewert:** `WorkflowExecutionResult | AsyncExecutionResult`
Wenn `async_execution=True`, wird sofort mit einer Task-ID zum Polling zurückgegeben. Andernfalls wird auf die Fertigstellung gewartet.
##### get_workflow_status()
Ruft den Status eines Workflows ab (Deployment-Status usw.).
```python
status = client.get_workflow_status("workflow-id")
print("Is deployed:", status.is_deployed)
```
**Parameter:**
- `workflow_id` (str): Die ID des Workflows
**Rückgabe:** `WorkflowStatus`
##### validate_workflow()
Überprüft, ob ein Workflow zur Ausführung bereit ist.
```python
is_ready = client.validate_workflow("workflow-id")
if is_ready:
# Workflow is deployed and ready
pass
```
**Parameter:**
- `workflow_id` (str): Die ID des Workflows
**Rückgabe:** `bool`
##### get_job_status()
Ruft den Status einer asynchronen Job-Ausführung ab.
```python
status = client.get_job_status("task-id-from-async-execution")
print("Status:", status["status"]) # 'queued', 'processing', 'completed', 'failed'
if status["status"] == "completed":
print("Output:", status["output"])
```
**Parameter:**
- `task_id` (str): Die Task-ID, die von der asynchronen Ausführung zurückgegeben wurde
**Rückgabe:** `Dict[str, Any]`
**Antwortfelder:**
- `success` (bool): Ob die Anfrage erfolgreich war
- `taskId` (str): Die Task-ID
- `status` (str): Einer von `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'`
- `metadata` (dict): Enthält `startedAt`, `completedAt` und `duration`
- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen)
- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen)
- `estimatedDuration` (int, optional): Geschätzte Dauer in Millisekunden (wenn in Bearbeitung/in Warteschlange)
##### execute_with_retry()
Führt einen Workflow mit automatischer Wiederholung bei Rate-Limit-Fehlern unter Verwendung von exponentiellem Backoff aus.
```python
result = client.execute_with_retry(
"workflow-id",
input_data={"message": "Hello"},
timeout=30.0,
max_retries=3, # Maximum number of retries
initial_delay=1.0, # Initial delay in seconds
max_delay=30.0, # Maximum delay in seconds
backoff_multiplier=2.0 # Exponential backoff multiplier
)
```
**Parameter:**
- `workflow_id` (str): Die ID des auszuführenden Workflows
- `input_data` (dict, optional): Eingabedaten, die an den Workflow übergeben werden
- `timeout` (float, optional): Timeout in Sekunden
- `stream` (bool, optional): Streaming-Antworten aktivieren
- `selected_outputs` (list, optional): Block-Ausgaben zum Streamen
- `async_execution` (bool, optional): Asynchron ausführen
- `max_retries` (int, optional): Maximale Anzahl von Wiederholungen (Standard: 3)
- `initial_delay` (float, optional): Anfangsverzögerung in Sekunden (Standard: 1.0)
- `max_delay` (float, optional): Maximale Verzögerung in Sekunden (Standard: 30.0)
- `backoff_multiplier` (float, optional): Backoff-Multiplikator (Standard: 2.0)
**Rückgabe:** `WorkflowExecutionResult | AsyncExecutionResult`
Die Wiederholungslogik verwendet exponentielles Backoff (1s → 2s → 4s → 8s...) mit ±25% Jitter, um Thundering Herd zu verhindern. Wenn die API einen `retry-after`-Header bereitstellt, wird dieser stattdessen verwendet.
##### get_rate_limit_info()
Ruft die aktuellen Rate-Limit-Informationen aus der letzten API-Antwort ab.
```python
rate_limit_info = client.get_rate_limit_info()
if rate_limit_info:
print("Limit:", rate_limit_info.limit)
print("Remaining:", rate_limit_info.remaining)
print("Reset:", datetime.fromtimestamp(rate_limit_info.reset))
```
**Rückgabewert:** `RateLimitInfo | None`
##### get_usage_limits()
Ruft aktuelle Nutzungslimits und Kontingentinformationen für Ihr Konto ab.
```python
limits = client.get_usage_limits()
print("Sync requests remaining:", limits.rate_limit["sync"]["remaining"])
print("Async requests remaining:", limits.rate_limit["async"]["remaining"])
print("Current period cost:", limits.usage["currentPeriodCost"])
print("Plan:", limits.usage["plan"])
```
**Rückgabewert:** `UsageLimits`
**Antwortstruktur:**
```python
{
"success": bool,
"rateLimit": {
"sync": {
"isLimited": bool,
"limit": int,
"remaining": int,
"resetAt": str
},
"async": {
"isLimited": bool,
"limit": int,
"remaining": int,
"resetAt": str
},
"authType": str # 'api' or 'manual'
},
"usage": {
"currentPeriodCost": float,
"limit": float,
"plan": str # e.g., 'free', 'pro'
}
}
```
##### set_api_key()
Aktualisiert den API-Schlüssel.
```python
client.set_api_key("new-api-key")
```
##### set_base_url()
Aktualisiert die Basis-URL.
```python
client.set_base_url("https://my-custom-domain.com")
```
##### close()
Schließt die zugrunde liegende HTTP-Sitzung.
```python
client.close()
```
## Datenklassen
### WorkflowExecutionResult
```python
@dataclass
class WorkflowExecutionResult:
success: bool
output: Optional[Any] = None
error: Optional[str] = None
logs: Optional[List[Any]] = None
metadata: Optional[Dict[str, Any]] = None
trace_spans: Optional[List[Any]] = None
total_duration: Optional[float] = None
```
### AsyncExecutionResult
```python
@dataclass
class AsyncExecutionResult:
success: bool
task_id: str
status: str # 'queued'
created_at: str
links: Dict[str, str] # e.g., {"status": "/api/jobs/{taskId}"}
```
### WorkflowStatus
```python
@dataclass
class WorkflowStatus:
is_deployed: bool
deployed_at: Optional[str] = None
needs_redeployment: bool = False
```
### RateLimitInfo
```python
@dataclass
class RateLimitInfo:
limit: int
remaining: int
reset: int
retry_after: Optional[int] = None
```
### UsageLimits
```python
@dataclass
class UsageLimits:
success: bool
rate_limit: Dict[str, Any]
usage: Dict[str, Any]
```
### SimStudioError
```python
class SimStudioError(Exception):
def __init__(self, message: str, code: Optional[str] = None, status: Optional[int] = None):
super().__init__(message)
self.code = code
self.status = status
```
**Häufige Fehlercodes:**
- `UNAUTHORIZED`: Ungültiger API-Schlüssel
- `TIMEOUT`: Zeitüberschreitung der Anfrage
- `RATE_LIMIT_EXCEEDED`: Ratenlimit überschritten
- `USAGE_LIMIT_EXCEEDED`: Nutzungslimit überschritten
- `EXECUTION_ERROR`: Workflow-Ausführung fehlgeschlagen
## Beispiele
### Grundlegende Workflow-Ausführung
<Steps>
<Step title="Client initialisieren">
Richten Sie den SimStudioClient mit Ihrem API-Schlüssel ein.
</Step>
<Step title="Workflow validieren">
Prüfen Sie, ob der Workflow bereitgestellt und zur Ausführung bereit ist.
</Step>
<Step title="Workflow ausführen">
Führen Sie den Workflow mit Ihren Eingabedaten aus.
</Step>
<Step title="Ergebnis verarbeiten">
Verarbeiten Sie das Ausführungsergebnis und behandeln Sie eventuelle Fehler.
</Step>
</Steps>
```python
import os
from simstudio import SimStudioClient
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def run_workflow():
try:
# Check if workflow is ready
is_ready = client.validate_workflow("my-workflow-id")
if not is_ready:
raise Exception("Workflow is not deployed or ready")
# Execute the workflow
result = client.execute_workflow(
"my-workflow-id",
input_data={
"message": "Process this data",
"user_id": "12345"
}
)
if result.success:
print("Output:", result.output)
print("Duration:", result.metadata.get("duration") if result.metadata else None)
else:
print("Workflow failed:", result.error)
except Exception as error:
print("Error:", error)
run_workflow()
```
### Fehlerbehandlung
Behandeln Sie verschiedene Fehlertypen, die während der Workflow-Ausführung auftreten können:
```python
from simstudio import SimStudioClient, SimStudioError
import os
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_with_error_handling():
try:
result = client.execute_workflow("workflow-id")
return result
except SimStudioError as error:
if error.code == "UNAUTHORIZED":
print("Invalid API key")
elif error.code == "TIMEOUT":
print("Workflow execution timed out")
elif error.code == "USAGE_LIMIT_EXCEEDED":
print("Usage limit exceeded")
elif error.code == "INVALID_JSON":
print("Invalid JSON in request body")
else:
print(f"Workflow error: {error}")
raise
except Exception as error:
print(f"Unexpected error: {error}")
raise
```
### Verwendung des Context-Managers
Verwenden Sie den Client als Context-Manager, um die Ressourcenbereinigung automatisch zu handhaben:
```python
from simstudio import SimStudioClient
import os
# Using context manager to automatically close the session
with SimStudioClient(api_key=os.getenv("SIM_API_KEY")) as client:
result = client.execute_workflow("workflow-id")
print("Result:", result)
# Session is automatically closed here
```
### Batch-Workflow-Ausführung
Führen Sie mehrere Workflows effizient aus:
```python
from simstudio import SimStudioClient
import os
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_workflows_batch(workflow_data_pairs):
"""Execute multiple workflows with different input data."""
results = []
for workflow_id, input_data in workflow_data_pairs:
try:
# Validate workflow before execution
if not client.validate_workflow(workflow_id):
print(f"Skipping {workflow_id}: not deployed")
continue
result = client.execute_workflow(workflow_id, input_data)
results.append({
"workflow_id": workflow_id,
"success": result.success,
"output": result.output,
"error": result.error
})
except Exception as error:
results.append({
"workflow_id": workflow_id,
"success": False,
"error": str(error)
})
return results
# Example usage
workflows = [
("workflow-1", {"type": "analysis", "data": "sample1"}),
("workflow-2", {"type": "processing", "data": "sample2"}),
]
results = execute_workflows_batch(workflows)
for result in results:
print(f"Workflow {result['workflow_id']}: {'Success' if result['success'] else 'Failed'}")
```
### Asynchrone Workflow-Ausführung
Führen Sie Workflows asynchron für langwierige Aufgaben aus:
```python
import os
import time
from simstudio import SimStudioClient
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_async():
try:
# Start async execution
result = client.execute_workflow(
"workflow-id",
input_data={"data": "large dataset"},
async_execution=True # Execute asynchronously
)
# Check if result is an async execution
if hasattr(result, 'task_id'):
print(f"Task ID: {result.task_id}")
print(f"Status endpoint: {result.links['status']}")
# Poll for completion
status = client.get_job_status(result.task_id)
while status["status"] in ["queued", "processing"]:
print(f"Current status: {status['status']}")
time.sleep(2) # Wait 2 seconds
status = client.get_job_status(result.task_id)
if status["status"] == "completed":
print("Workflow completed!")
print(f"Output: {status['output']}")
print(f"Duration: {status['metadata']['duration']}")
else:
print(f"Workflow failed: {status['error']}")
except Exception as error:
print(f"Error: {error}")
execute_async()
```
### Ratenlimitierung und Wiederholung
Behandeln Sie Ratenbegrenzungen automatisch mit exponentiellem Backoff:
```python
import os
from simstudio import SimStudioClient, SimStudioError
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_with_retry_handling():
try:
# Automatically retries on rate limit
result = client.execute_with_retry(
"workflow-id",
input_data={"message": "Process this"},
max_retries=5,
initial_delay=1.0,
max_delay=60.0,
backoff_multiplier=2.0
)
print(f"Success: {result}")
except SimStudioError as error:
if error.code == "RATE_LIMIT_EXCEEDED":
print("Rate limit exceeded after all retries")
# Check rate limit info
rate_limit_info = client.get_rate_limit_info()
if rate_limit_info:
from datetime import datetime
reset_time = datetime.fromtimestamp(rate_limit_info.reset)
print(f"Rate limit resets at: {reset_time}")
execute_with_retry_handling()
```
### Nutzungsüberwachung
Überwachen Sie die Nutzung und Limits Ihres Kontos:
```python
import os
from simstudio import SimStudioClient
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def check_usage():
try:
limits = client.get_usage_limits()
print("=== Rate Limits ===")
print("Sync requests:")
print(f" Limit: {limits.rate_limit['sync']['limit']}")
print(f" Remaining: {limits.rate_limit['sync']['remaining']}")
print(f" Resets at: {limits.rate_limit['sync']['resetAt']}")
print(f" Is limited: {limits.rate_limit['sync']['isLimited']}")
print("\nAsync requests:")
print(f" Limit: {limits.rate_limit['async']['limit']}")
print(f" Remaining: {limits.rate_limit['async']['remaining']}")
print(f" Resets at: {limits.rate_limit['async']['resetAt']}")
print(f" Is limited: {limits.rate_limit['async']['isLimited']}")
print("\n=== Usage ===")
print(f"Current period cost: ${limits.usage['currentPeriodCost']:.2f}")
print(f"Limit: ${limits.usage['limit']:.2f}")
print(f"Plan: {limits.usage['plan']}")
percent_used = (limits.usage['currentPeriodCost'] / limits.usage['limit']) * 100
print(f"Usage: {percent_used:.1f}%")
if percent_used > 80:
print("⚠️ Warning: You are approaching your usage limit!")
except Exception as error:
print(f"Error checking usage: {error}")
check_usage()
```
### Streaming-Workflow-Ausführung
Führen Sie Workflows mit Echtzeit-Streaming-Antworten aus:
```python
from simstudio import SimStudioClient
import os
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_with_streaming():
"""Execute workflow with streaming enabled."""
try:
# Enable streaming for specific block outputs
result = client.execute_workflow(
"workflow-id",
input_data={"message": "Count to five"},
stream=True,
selected_outputs=["agent1.content"] # Use blockName.attribute format
)
print("Workflow result:", result)
except Exception as error:
print("Error:", error)
execute_with_streaming()
```
Die Streaming-Antwort folgt dem Server-Sent-Events- (SSE-) Format:
```
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":", two"}
data: {"event":"done","success":true,"output":{},"metadata":{"duration":610}}
data: [DONE]
```
**Flask-Streaming-Beispiel:**
```python
from flask import Flask, Response, stream_with_context
import requests
import json
import os
app = Flask(__name__)
@app.route('/stream-workflow')
def stream_workflow():
"""Stream workflow execution to the client."""
def generate():
response = requests.post(
'https://sim.ai/api/workflows/WORKFLOW_ID/execute',
headers={
'Content-Type': 'application/json',
'X-API-Key': os.getenv('SIM_API_KEY')
},
json={
'message': 'Generate a story',
'stream': True,
'selectedOutputs': ['agent1.content']
},
stream=True
)
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data: '):
data = decoded_line[6:] # Remove 'data: ' prefix
if data == '[DONE]':
break
try:
parsed = json.loads(data)
if 'chunk' in parsed:
yield f"data: {json.dumps(parsed)}\n\n"
elif parsed.get('event') == 'done':
yield f"data: {json.dumps(parsed)}\n\n"
print("Execution complete:", parsed.get('metadata'))
except json.JSONDecodeError:
pass
return Response(
stream_with_context(generate()),
mimetype='text/event-stream'
)
if __name__ == '__main__':
app.run(debug=True)
```
### Umgebungs­konfiguration
Konfigurieren Sie den Client mit Umgebungsvariablen:
<Tabs items={['Development', 'Production']}>
<Tab value="Development">
```python
import os
from simstudio import SimStudioClient
# Development configuration
client = SimStudioClient(
api_key=os.getenv("SIM_API_KEY")
base_url=os.getenv("SIM_BASE_URL", "https://sim.ai")
)
```
</Tab>
<Tab value="Production">
```python
import os
from simstudio import SimStudioClient
# Production configuration with error handling
api_key = os.getenv("SIM_API_KEY")
if not api_key:
raise ValueError("SIM_API_KEY environment variable is required")
client = SimStudioClient(
api_key=api_key,
base_url=os.getenv("SIM_BASE_URL", "https://sim.ai")
)
```
</Tab>
</Tabs>
## Ihren API-Schlüssel erhalten
<Steps>
<Step title="Bei Sim anmelden">
Navigieren Sie zu [Sim](https://sim.ai) und melden Sie sich in Ihrem Konto an.
</Step>
<Step title="Workflow öffnen">
Navigieren Sie zu dem Workflow, den Sie programmatisch ausführen möchten.
</Step>
<Step title="Workflow bereitstellen">
Klicken Sie auf "Bereitstellen", um Ihren Workflow bereitzustellen, falls dies noch nicht geschehen ist.
</Step>
<Step title="API-Schlüssel erstellen oder auswählen">
Wählen oder erstellen Sie während des Bereitstellungsprozesses einen API-Schlüssel.
</Step>
<Step title="API-Schlüssel kopieren">
Kopieren Sie den API-Schlüssel, um ihn in Ihrer Python-Anwendung zu verwenden.
</Step>
</Steps>
## Voraussetzungen
- Python 3.8+
- requests >= 2.25.0
## Lizenz
Apache-2.0

File diff suppressed because it is too large Load Diff

View File

@@ -152,9 +152,3 @@ Input → Agent (Google Search, Notion) → Function (Compile Report)
- **Sei spezifisch in System-Prompts**: Definiere die Rolle, den Ton und die Einschränkungen des Agenten klar. Je spezifischer deine Anweisungen sind, desto besser kann der Agent seinen vorgesehenen Zweck erfüllen.
- **Wähle die richtige Temperatureinstellung**: Verwende niedrigere Temperatureinstellungen (0-0,3), wenn Genauigkeit wichtig ist, oder erhöhe die Temperatur (0,7-2,0) für kreativere oder vielfältigere Antworten
- **Nutze Tools effektiv**: Integriere Tools, die den Zweck des Agenten ergänzen und seine Fähigkeiten erweitern. Sei selektiv bei der Auswahl der Tools, um den Agenten nicht zu überfordern. Für Aufgaben mit wenig Überschneidung verwende einen anderen Agent-Block für die besten Ergebnisse.
## Best Practices
- **Seien Sie spezifisch in System-Prompts**: Definieren Sie die Rolle, den Ton und die Grenzen des Agenten klar. Je spezifischer Ihre Anweisungen sind, desto besser kann der Agent seinen beabsichtigten Zweck erfüllen.
- **Wählen Sie die richtige Temperatureinstellung**: Verwenden Sie niedrigere Temperatureinstellungen (00,3), wenn Genauigkeit wichtig ist, oder erhöhen Sie die Temperatur (0,72,0) für kreativere oder vielfältigere Antworten
- **Nutzen Sie Tools effektiv**: Integrieren Sie Tools, die den Zweck des Agenten ergänzen und seine Fähigkeiten erweitern. Seien Sie selektiv bei der Auswahl der Tools, um den Agenten nicht zu überfordern. Verwenden Sie für Aufgaben mit geringer Überschneidung einen weiteren Agent-Block für die besten Ergebnisse.

View File

@@ -190,8 +190,13 @@ console.log(`${processedItems} gültige Elemente verarbeitet`);
### Einschränkungen
<Callout type="info">
Container-Blöcke (Schleifen und Parallele) unterstützen Verschachtelung. Du kannst Schleifen in Schleifen, Parallele in Schleifen und jede Kombination von Container-Blöcken platzieren, um komplexe mehrdimensionale Workflows zu erstellen.
<Callout type="warning">
Container-Blöcke (Schleifen und Parallele) können nicht ineinander verschachtelt werden. Das bedeutet:
- Du kannst keinen Schleifenblock in einen anderen Schleifenblock platzieren
- Du kannst keinen Parallel-Block in einen Schleifenblock platzieren
- Du kannst keinen Container-Block in einen anderen Container-Block platzieren
Wenn du mehrdimensionale Iterationen benötigst, erwäge eine Umstrukturierung deines Workflows, um sequentielle Schleifen zu verwenden oder Daten in Stufen zu verarbeiten.
</Callout>
<Callout type="info">
@@ -250,57 +255,3 @@ console.log(`${processedItems} gültige Elemente verarbeitet`);
- **Setzen Sie vernünftige Grenzen**: Halten Sie die Anzahl der Iterationen in einem vernünftigen Rahmen, um lange Ausführungszeiten zu vermeiden
- **Verwenden Sie ForEach für Sammlungen**: Verwenden Sie beim Verarbeiten von Arrays oder Objekten ForEach anstelle von For-Schleifen
- **Behandeln Sie Fehler elegant**: Erwägen Sie, Fehlerbehandlung innerhalb von Schleifen hinzuzufügen, um robuste Workflows zu gewährleisten
## Eingaben und Ausgaben
<Tabs items={['Configuration', 'Variables', 'Results']}>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>Schleifentyp</strong>: Wählen Sie zwischen 'for', 'forEach', 'while' oder 'doWhile'
</li>
<li>
<strong>Iterationen</strong>: Anzahl der Ausführungen (für for-Schleifen)
</li>
<li>
<strong>Sammlung</strong>: Array oder Objekt zum Durchlaufen (für forEach-Schleifen)
</li>
<li>
<strong>Bedingung</strong>: Boolescher Ausdruck zur Auswertung (für while/do-while-Schleifen)
</li>
</ul>
</Tab>
<Tab>
Verfügbar **innerhalb** der Schleife:
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<loop.index>"}</strong>: Aktuelle Iterationsnummer (0-basiert)
</li>
<li>
<strong>{"<loop.currentItem>"}</strong>: Aktuell verarbeitetes Element (nur forEach)
</li>
<li>
<strong>{"<loop.items>"}</strong>: Vollständige Sammlung (nur forEach)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<blockname.results>"}</strong>: Array aller Iterationsergebnisse (Zugriff über Blocknamen)
</li>
<li>
<strong>Struktur</strong>: Ergebnisse behalten die Iterationsreihenfolge bei
</li>
<li>
<strong>Zugriff</strong>: Verfügbar in Blöcken nach Abschluss der Schleife
</li>
</ul>
</Tab>
</Tabs>
## Best Practices
- **Setzen Sie vernünftige Grenzen**: Halten Sie die Iterationsanzahl angemessen, um lange Ausführungszeiten zu vermeiden
- **Verwenden Sie ForEach für Sammlungen**: Verwenden Sie beim Verarbeiten von Arrays oder Objekten ForEach anstelle von For-Schleifen
- **Behandeln Sie Fehler elegant**: Erwägen Sie, Fehlerbehandlung innerhalb von Schleifen hinzuzufügen, um robuste Workflows zu gewährleisten

View File

@@ -142,8 +142,11 @@ Jede parallele Instanz läuft unabhängig:
### Einschränkungen
<Callout type="info">
Container-Blöcke (Schleifen und Parallele) unterstützen Verschachtelung. Sie können Parallele in Parallele, Schleifen in Parallele und jede Kombination von Container-Blöcken platzieren, um komplexe mehrdimensionale Workflows zu erstellen.
<Callout type="warning">
Container-Blöcke (Schleifen und Parallele) können nicht ineinander verschachtelt werden. Das bedeutet:
- Sie können keinen Schleifenblock in einen Parallelblock platzieren
- Sie können keinen weiteren Parallelblock in einen Parallelblock platzieren
- Sie können keinen Container-Block in einen anderen Container-Block platzieren
</Callout>
<Callout type="info">
@@ -211,51 +214,3 @@ Wann Sie welche Methode verwenden sollten:
- **Nur unabhängige Operationen**: Stellen Sie sicher, dass Operationen nicht voneinander abhängen
- **Ratenbegrenzungen berücksichtigen**: Fügen Sie Verzögerungen oder Drosselungen für API-intensive Workflows hinzu
- **Fehlerbehandlung**: Jede Instanz sollte ihre eigenen Fehler angemessen behandeln
## Eingaben und Ausgaben
<Tabs items={['Konfiguration', 'Variablen', 'Ergebnisse']}>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>Parallel-Typ</strong>: Wählen Sie zwischen „count" oder „collection"
</li>
<li>
<strong>Anzahl</strong>: Anzahl der auszuführenden Instanzen (anzahlbasiert)
</li>
<li>
<strong>Collection</strong>: Array oder Objekt zur Verteilung (sammlungsbasiert)
</li>
</ul>
</Tab>
<Tab>
Verfügbar **innerhalb** der Parallelverarbeitung:
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<parallel.index>"}</strong>: Instanznummer (0-basiert)
</li>
<li>
<strong>{"<parallel.currentItem>"}</strong>: Element für diese Instanz (nur sammlungsbasiert)
</li>
<li>
<strong>{"<parallel.items>"}</strong>: Vollständige Sammlung (nur sammlungsbasiert)
</li>
</ul>
</Tab>
<Tab>
<ul className="list-disc space-y-2 pl-6">
<li>
<strong>{"<blockname.results>"}</strong>: Array aller Instanzergebnisse (Zugriff über Blockname)
</li>
<li>
<strong>Zugriff</strong>: Verfügbar in Blöcken nach Abschluss der Parallelverarbeitung
</li>
</ul>
</Tab>
</Tabs>
## Best Practices
- **Nur unabhängige Operationen**: Stellen Sie sicher, dass Operationen nicht voneinander abhängen
- **Rate Limits beachten**: Fügen Sie Verzögerungen oder Drosselung für API-intensive Workflows hinzu
- **Fehlerbehandlung**: Jede Instanz sollte ihre eigenen Fehler ordnungsgemäß behandeln

View File

@@ -100,18 +100,3 @@ Input (Lead) → Router → Agent (Enterprise Sales) or Workflow (Self-serve)
- **Mit verschiedenen Eingaben testen**: Stellen Sie sicher, dass der Router verschiedene Eingabetypen, Grenzfälle und unerwartete Inhalte verarbeiten kann
- **Routing-Leistung überwachen**: Überprüfen Sie Routing-Entscheidungen regelmäßig und verfeinern Sie Kriterien basierend auf tatsächlichen Nutzungsmustern
- **Geeignete Modelle auswählen**: Verwenden Sie Modelle mit starken Argumentationsfähigkeiten für komplexe Routing-Entscheidungen
Wenn der Router keine geeignete Route für den gegebenen Kontext ermitteln kann, leitet er stattdessen zum **Fehlerpfad** weiter, anstatt willkürlich eine Route auszuwählen. Dies geschieht, wenn:
- Der Kontext keiner der definierten Routenbeschreibungen eindeutig entspricht
- Die KI feststellt, dass keine der verfügbaren Routen geeignet ist
## Best Practices
- **Klare Routenbeschreibungen verfassen**: Jede Routenbeschreibung sollte klar erklären, wann diese Route ausgewählt werden sollte. Seien Sie spezifisch bezüglich der Kriterien.
- **Routen gegenseitig ausschließend gestalten**: Stellen Sie nach Möglichkeit sicher, dass sich Routenbeschreibungen nicht überschneiden, um mehrdeutige Routing-Entscheidungen zu vermeiden.
- **Einen Fehlerpfad verbinden**: Behandeln Sie Fälle, in denen keine Route passt, indem Sie einen Fehlerbehandler für ein elegantes Fallback-Verhalten verbinden.
- **Aussagekräftige Routentitel verwenden**: Routentitel erscheinen im Workflow-Canvas, machen Sie sie daher für bessere Lesbarkeit aussagekräftig.
- **Mit verschiedenen Eingaben testen**: Stellen Sie sicher, dass der Router verschiedene Eingabetypen, Grenzfälle und unerwartete Inhalte verarbeitet.
- **Routing-Performance überwachen**: Überprüfen Sie Routing-Entscheidungen regelmäßig und verfeinern Sie Routenbeschreibungen basierend auf tatsächlichen Nutzungsmustern.
- **Geeignete Modelle wählen**: Verwenden Sie Modelle mit starken Reasoning-Fähigkeiten für komplexe Routing-Entscheidungen.

View File

@@ -169,175 +169,3 @@ copilotCost = (inputTokens × inputPrice + outputTokens × (outputPrice × 1.5))
<Callout type="info">
Modellpreise werden pro Million Tokens angegeben. Die Berechnung teilt durch 1.000.000, um die tatsächlichen Kosten zu ermitteln. Siehe <a href="/execution/costs">die Seite zur Kostenberechnung</a> für Hintergründe und Beispiele.
</Callout>
Fahre mit der Maus über eine deiner Nachrichten und klicke auf **Bearbeiten**, um sie zu ändern und erneut zu senden. Dies ist nützlich, um deine Eingaben zu verfeinern.
### Nachrichtenwarteschlange
Wenn du eine Nachricht sendest, während Copilot noch antwortet, wird sie in die Warteschlange gestellt. Du kannst:
- Warteschlangennachrichten im erweiterbaren Warteschlangenpanel anzeigen
- Eine Nachricht aus der Warteschlange sofort senden (bricht die aktuelle Antwort ab)
- Nachrichten aus der Warteschlange entfernen
## Dateianhänge
Klicke auf das Anhang-Symbol, um Dateien mit deiner Nachricht hochzuladen. Unterstützte Dateitypen umfassen:
- Bilder (Vorschau-Thumbnails werden angezeigt)
- PDFs
- Textdateien, JSON, XML
- Andere Dokumentformate
Dateien werden als anklickbare Thumbnails angezeigt, die in einem neuen Tab geöffnet werden.
## Checkpoints & Änderungen
Wenn Copilot Änderungen an deinem Workflow vornimmt, speichert es Checkpoints, damit du bei Bedarf zurückkehren kannst.
### Checkpoints anzeigen
Fahre mit der Maus über eine Copilot-Nachricht und klicke auf das Checkpoints-Symbol, um gespeicherte Workflow-Zustände für diese Nachricht anzuzeigen.
### Änderungen rückgängig machen
Klicke bei jedem Checkpoint auf **Rückgängig machen**, um deinen Workflow auf diesen Zustand zurückzusetzen. Ein Bestätigungsdialog warnt dich, dass diese Aktion nicht rückgängig gemacht werden kann.
### Änderungen akzeptieren
Wenn Copilot Änderungen vorschlägt, kannst du:
- **Akzeptieren**: Die vorgeschlagenen Änderungen anwenden (`Mod+Shift+Enter`)
- **Ablehnen**: Die Änderungen verwerfen und deinen aktuellen Workflow beibehalten
## Denkblöcke
Bei komplexen Anfragen kann Copilot seinen Denkprozess in erweiterbaren Denkblöcken anzeigen:
- Blöcke werden automatisch erweitert, während Copilot denkt
- Klicken zum manuellen Erweitern/Reduzieren
- Zeigt die Dauer des Denkprozesses an
- Hilft dir zu verstehen, wie Copilot zu seiner Lösung gekommen ist
## Optionsauswahl
Wenn Copilot mehrere Optionen präsentiert, kannst du auswählen mit:
| Steuerung | Aktion |
|---------|--------|
| **1-9** | Option nach Nummer auswählen |
| **Pfeiltaste auf/ab** | Zwischen Optionen navigieren |
| **Eingabetaste** | Hervorgehobene Option auswählen |
Ausgewählte Optionen sind hervorgehoben; nicht ausgewählte Optionen erscheinen durchgestrichen.
## Tastenkombinationen
| Tastenkombination | Aktion |
|----------|--------|
| `@` | Kontextmenü öffnen |
| `/` | Slash-Befehle öffnen |
| `Arrow Up/Down` | Menüelemente navigieren |
| `Enter` | Menüelement auswählen |
| `Esc` | Menüs schließen |
| `Mod+Shift+Enter` | Copilot-Änderungen akzeptieren |
## Nutzungslimits
Die Copilot-Nutzung wird pro Token des zugrunde liegenden LLM abgerechnet. Wenn Sie Ihr Nutzungslimit erreichen, fordert Copilot Sie auf, Ihr Limit zu erhöhen. Sie können die Nutzung in Schritten (50 $, 100 $) von Ihrer aktuellen Basis aus hinzufügen.
<Callout type="info">
Siehe die [Seite zur Kostenberechnung](/execution/costs) für Abrechnungsdetails.
</Callout>
## Copilot MCP
Sie können Copilot als MCP-Server in Ihrem bevorzugten Editor oder AI-Client verwenden. Damit können Sie Sim-Workflows direkt aus Tools wie Cursor, Claude Code, Claude Desktop und VS Code erstellen, testen, bereitstellen und verwalten.
### Generieren eines Copilot-API-Schlüssels
Um sich mit dem Copilot-MCP-Server zu verbinden, benötigen Sie einen **Copilot-API-Schlüssel**:
1. Gehen Sie zu [sim.ai](https://sim.ai) und melden Sie sich an
2. Navigieren Sie zu **Einstellungen** → **Copilot**
3. Klicken Sie auf **API-Schlüssel generieren**
4. Kopieren Sie den Schlüssel er wird nur einmal angezeigt
Der Schlüssel sieht aus wie `sk-sim-copilot-...`. Sie werden ihn in der folgenden Konfiguration verwenden.
### Cursor
Fügen Sie Folgendes zu Ihrer `.cursor/mcp.json` (Projektebene) oder den globalen Cursor-MCP-Einstellungen hinzu:
```json
{
"mcpServers": {
"sim-copilot": {
"url": "https://www.sim.ai/api/mcp/copilot",
"headers": {
"X-API-Key": "YOUR_COPILOT_API_KEY"
}
}
}
}
```
Ersetzen Sie `YOUR_COPILOT_API_KEY` durch den oben generierten Schlüssel.
### Claude Code
Führen Sie den folgenden Befehl aus, um den Copilot MCP-Server hinzuzufügen:
```bash
claude mcp add sim-copilot \
--transport http \
https://www.sim.ai/api/mcp/copilot \
--header "X-API-Key: YOUR_COPILOT_API_KEY"
```
Ersetzen Sie `YOUR_COPILOT_API_KEY` durch Ihren Schlüssel.
### Claude Desktop
Claude Desktop benötigt [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), um sich mit HTTP-basierten MCP-Servern zu verbinden. Fügen Sie Folgendes zu Ihrer Claude Desktop-Konfigurationsdatei hinzu (`~/Library/Application Support/Claude/claude_desktop_config.json` unter macOS):
```json
{
"mcpServers": {
"sim-copilot": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://www.sim.ai/api/mcp/copilot",
"--header",
"X-API-Key: YOUR_COPILOT_API_KEY"
]
}
}
}
```
Ersetzen Sie `YOUR_COPILOT_API_KEY` durch Ihren Schlüssel.
### VS Code
Fügen Sie Folgendes zu Ihrer VS Code `settings.json` oder Workspace `.vscode/settings.json` hinzu:
```json
{
"mcp": {
"servers": {
"sim-copilot": {
"type": "http",
"url": "https://www.sim.ai/api/mcp/copilot",
"headers": {
"X-API-Key": "YOUR_COPILOT_API_KEY"
}
}
}
}
}
```
Ersetzen Sie `YOUR_COPILOT_API_KEY` durch Ihren Schlüssel.
<Callout type="info">
Für selbst gehostete Deployments ersetzen Sie `https://www.sim.ai` durch Ihre selbst gehostete Sim-URL.
</Callout>

View File

@@ -75,40 +75,3 @@ Für selbst gehostete Bereitstellungen können Enterprise-Funktionen über Umgeb
<Callout type="warn">
BYOK ist nur im gehosteten Sim verfügbar. Selbst gehostete Deployments konfigurieren AI-Provider-Schlüssel direkt über Umgebungsvariablen.
</Callout>
Wenn die Abrechnung deaktiviert ist, verwenden Sie die Admin-API zur Verwaltung von Organisationen:
```bash
# Create an organization
curl -X POST https://your-instance/api/v1/admin/organizations \
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My Organization", "ownerId": "user-id-here"}'
# Add a member
curl -X POST https://your-instance/api/v1/admin/organizations/{orgId}/members \
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"userId": "user-id-here", "role": "admin"}'
```
### Workspace-Mitglieder
Wenn Einladungen deaktiviert sind, verwenden Sie die Admin-API zur direkten Verwaltung von Workspace-Mitgliedschaften:
```bash
# Add a user to a workspace
curl -X POST https://your-instance/api/v1/admin/workspaces/{workspaceId}/members \
-H "x-admin-key: YOUR_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"userId": "user-id-here", "permissions": "write"}'
# Remove a user from a workspace
curl -X DELETE "https://your-instance/api/v1/admin/workspaces/{workspaceId}/members?userId=user-id-here" \
-H "x-admin-key: YOUR_ADMIN_API_KEY"
```
### Hinweise
- Die Aktivierung von `ACCESS_CONTROL_ENABLED` aktiviert automatisch Organisationen, da die Zugriffskontrolle eine Organisationsmitgliedschaft erfordert.
- Wenn `DISABLE_INVITATIONS` gesetzt ist, können Benutzer keine Einladungen versenden. Verwenden Sie stattdessen die Admin-API zur Verwaltung von Workspace- und Organisationsmitgliedschaften.

View File

@@ -241,45 +241,4 @@ Dies verteilt große Mehrverbrauchsgebühren über den Monat, anstatt einer gro
- Überprüfen Sie Ihre aktuelle Nutzung unter [Einstellungen → Abonnement](https://sim.ai/settings/subscription)
- Erfahren Sie mehr über [Protokollierung](/execution/logging), um Ausführungsdetails zu verfolgen
- Entdecken Sie die [externe API](/execution/api) für programmatische Kostenüberwachung
- Sehen Sie sich [Workflow-Optimierungstechniken](/blocks) an, um Kosten zu reduzieren
**Pro-Tarif (20 $/Monat):**
- Monatliches Abonnement beinhaltet 20 $ Nutzung
- Nutzung unter 20 $ → Keine zusätzlichen Gebühren
- Nutzung über 20 $ → Mehrverbrauch wird am Monatsende abgerechnet
- Beispiel: 35 $ Nutzung = 20 $ (Abonnement) + 15 $ (Mehrverbrauch)
**Team-Tarif (40 $/Platz/Monat):**
- Gemeinsame Nutzung über alle Teammitglieder hinweg
- Mehrverbrauch wird aus der gesamten Teamnutzung berechnet
- Der Organisationsinhaber erhält eine Rechnung
**Enterprise-Tarife:**
- Fester Monatspreis, keine Mehrverbräuche
- Individuelle Nutzungslimits gemäß Vereinbarung
### Schwellenwertabrechnung
Wenn der nicht abgerechnete Mehrverbrauch 50 $ erreicht, rechnet Sim automatisch den gesamten nicht abgerechneten Betrag ab.
**Beispiel:**
- Tag 10: 70 $ Mehrverbrauch → 70 $ sofort abrechnen
- Tag 15: Weitere 35 $ Nutzung (105 $ gesamt) → Bereits abgerechnet, keine Aktion
- Tag 20: Weitere 50 $ Nutzung (155 $ gesamt, 85 $ nicht abgerechnet) → 85 $ sofort abrechnen
Dies verteilt hohe Mehrverbrauchsgebühren über den Monat hinweg, anstatt einer großen Rechnung am Periodenende.
## Best Practices für das Kostenmanagement
1. **Regelmäßig überwachen**: Überprüfen Sie Ihr Nutzungs-Dashboard häufig, um Überraschungen zu vermeiden
2. **Budgets festlegen**: Nutzen Sie Tariflimits als Leitplanken für Ihre Ausgaben
3. **Workflows optimieren**: Überprüfen Sie kostenintensive Ausführungen und optimieren Sie Prompts oder Modellauswahl
4. **Passende Modelle verwenden**: Stimmen Sie die Modellkomplexität auf die Aufgabenanforderungen ab
5. **Ähnliche Aufgaben bündeln**: Kombinieren Sie mehrere Anfragen, wenn möglich, um den Overhead zu reduzieren
## Nächste Schritte
- Überprüfen Sie Ihre aktuelle Nutzung unter [Einstellungen → Abonnement](https://sim.ai/settings/subscription)
- Erfahren Sie mehr über [Protokollierung](/execution/logging), um Ausführungsdetails zu verfolgen
- Erkunden Sie die [externe API](/execution/api) für programmatische Kostenüberwachung
- Informieren Sie sich über [Workflow-Optimierungstechniken](/blocks), um Kosten zu reduzieren
- Sehen Sie sich [Workflow-Optimierungstechniken](/blocks) an, um Kosten zu reduzieren

View File

@@ -1,172 +0,0 @@
---
title: Dateien übergeben
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Sim macht es einfach, mit Dateien in Ihren Workflows zu arbeiten. Blöcke können Dateien empfangen, verarbeiten und nahtlos an andere Blöcke weitergeben.
## Dateiobjekte
Wenn Blöcke Dateien ausgeben (wie Gmail-Anhänge, generierte Bilder oder geparste Dokumente), geben sie ein standardisiertes Dateiobjekt zurück:
```json
{
"name": "report.pdf",
"url": "https://...",
"base64": "JVBERi0xLjQK...",
"type": "application/pdf",
"size": 245678
}
```
Sie können auf alle diese Eigenschaften zugreifen, wenn Sie auf Dateien aus vorherigen Blöcken verweisen.
## Der Datei-Block
Der **Datei-Block** ist der universelle Einstiegspunkt für Dateien in Ihren Workflows. Er akzeptiert Dateien aus jeder Quelle und gibt standardisierte Dateiobjekte aus, die mit allen Integrationen funktionieren.
**Eingaben:**
- **Hochgeladene Dateien** - Dateien direkt per Drag & Drop oder Auswahl hinzufügen
- **Externe URLs** - Jede öffentlich zugängliche Datei-URL
- **Dateien von anderen Blöcken** - Dateien von Gmail-Anhängen, Slack-Downloads usw. übergeben
**Ausgaben:**
- Eine Liste von `UserFile`-Objekten mit konsistenter Struktur (`name`, `url`, `base64`, `type`, `size`)
- `combinedContent` - Extrahierter Textinhalt aus allen Dateien (für Dokumente)
**Beispielverwendung:**
```
// Get all files from the File block
<file.files>
// Get the first file
<file.files[0]>
// Get combined text content from parsed documents
<file.combinedContent>
```
Der Datei-Block führt automatisch folgende Aktionen aus:
- Erkennt Dateitypen aus URLs und Erweiterungen
- Extrahiert Text aus PDFs, CSVs und Dokumenten
- Generiert Base64-Kodierung für Binärdateien
- Erstellt vorsignierte URLs für sicheren Zugriff
Verwenden Sie den Datei-Block, wenn Sie Dateien aus verschiedenen Quellen normalisieren müssen, bevor Sie sie an andere Blöcke wie Vision, STT oder E-Mail-Integrationen übergeben.
## Dateien zwischen Blöcken übergeben
Verweisen Sie auf Dateien aus vorherigen Blöcken über das Tag-Dropdown. Klicken Sie in ein beliebiges Dateieingabefeld und geben Sie `<` ein, um verfügbare Ausgaben anzuzeigen.
**Häufige Muster:**
```
// Single file from a block
<gmail.attachments[0]>
// Pass the whole file object
<file_parser.files[0]>
// Access specific properties
<gmail.attachments[0].name>
<gmail.attachments[0].base64>
```
Die meisten Blöcke akzeptieren das vollständige Dateiobjekt und extrahieren automatisch, was sie benötigen. Sie müssen `base64` oder `url` in den meisten Fällen nicht manuell extrahieren.
## Workflows mit Dateien auslösen
Wenn Sie einen Workflow über die API aufrufen, der Dateieingaben erwartet, fügen Sie Dateien in Ihre Anfrage ein:
<Tabs items={['Base64', 'URL']}>
<Tab value="Base64">
```bash
curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"document": {
"name": "report.pdf",
"base64": "JVBERi0xLjQK...",
"type": "application/pdf"
}
}'
```
</Tab>
<Tab value="URL">
```bash
curl -X POST "https://sim.ai/api/workflows/YOUR_WORKFLOW_ID/execute" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"document": {
"name": "report.pdf",
"url": "https://example.com/report.pdf",
"type": "application/pdf"
}
}'
```
</Tab>
</Tabs>
Der Start-Block des Workflows sollte ein Eingabefeld haben, das für den Empfang des Dateiparameters konfiguriert ist.
## Dateien in API-Antworten empfangen
Wenn ein Workflow Dateien ausgibt, sind diese in der Antwort enthalten:
```json
{
"success": true,
"output": {
"generatedFile": {
"name": "output.png",
"url": "https://...",
"base64": "iVBORw0KGgo...",
"type": "image/png",
"size": 34567
}
}
}
```
Verwenden Sie `url` für direkte Downloads oder `base64` für Inline-Verarbeitung.
## Blöcke, die mit Dateien arbeiten
**Dateieingaben:**
- **File** - Dokumente, Bilder und Textdateien parsen
- **Vision** - Bilder mit KI-Modellen analysieren
- **Mistral Parser** - Text aus PDFs extrahieren
**Dateiausgaben:**
- **Gmail** - E-Mail-Anhänge
- **Slack** - Heruntergeladene Dateien
- **TTS** - Generierte Audiodateien
- **Video Generator** - Generierte Videos
- **Image Generator** - Generierte Bilder
**Dateispeicherung:**
- **Supabase** - Upload/Download aus dem Speicher
- **S3** - AWS S3-Operationen
- **Google Drive** - Drive-Dateioperationen
- **Dropbox** - Dropbox-Dateioperationen
<Callout type="info">
Dateien sind automatisch für nachgelagerte Blöcke verfügbar. Die Ausführungs-Engine übernimmt die gesamte Dateiübertragung und Formatkonvertierung.
</Callout>
## Best Practices
1. **Dateiobjekte direkt verwenden** - Übergeben Sie das vollständige Dateiobjekt, anstatt einzelne Eigenschaften zu extrahieren. Blöcke übernehmen die Konvertierung automatisch.
2. **Dateitypen prüfen** - Stellen Sie sicher, dass der Dateityp mit dem übereinstimmt, was der empfangende Block erwartet. Der Vision-Block benötigt Bilder, der File-Block verarbeitet Dokumente.
3. **Dateigröße beachten** Große Dateien erhöhen die Ausführungszeit. Bei sehr großen Dateien sollten Sie Storage-Blöcke (S3, Supabase) für die Zwischenspeicherung verwenden.

View File

@@ -1,142 +0,0 @@
---
title: Formular-Bereitstellung
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Stellen Sie Ihren Workflow als einbettbares Formular bereit, das Benutzer auf Ihrer Website ausfüllen oder per Link teilen können. Formularübermittlungen lösen Ihren Workflow mit dem `form` Trigger-Typ aus.
## Übersicht
Die Formular-Bereitstellung verwandelt das Eingabeformat Ihres Workflows in ein responsives Formular, das:
- Per Direktlink geteilt werden kann (z. B. `https://sim.ai/form/my-survey`)
- Mit einem iframe in jede Website eingebettet werden kann
Wenn ein Benutzer das Formular absendet, wird Ihr Workflow mit den Formulardaten ausgelöst.
<Callout type="info">
Formulare leiten ihre Felder vom Eingabeformat des Start-Blocks Ihres Workflows ab. Jedes Feld wird zu einer Formulareingabe mit dem entsprechenden Typ.
</Callout>
## Erstellen eines Formulars
1. Öffnen Sie Ihren Workflow und klicken Sie auf **Bereitstellen**
2. Wählen Sie den Tab **Formular**
3. Konfigurieren Sie:
- **URL**: Eindeutige Kennung (z. B. `contact-form` → `sim.ai/form/contact-form`)
- **Titel**: Formularüberschrift
- **Beschreibung**: Optionaler Untertitel
- **Formularfelder**: Passen Sie Beschriftungen und Beschreibungen für jedes Feld an
- **Authentifizierung**: Öffentlich, passwortgeschützt oder E-Mail-Whitelist
- **Dankesnachricht**: Wird nach der Übermittlung angezeigt
4. Klicken Sie auf **Starten**
## Feldzuordnung
| Eingabeformat-Typ | Formularfeld |
|------------------|------------|
| `string` | Texteingabe |
| `number` | Zahleneingabe |
| `boolean` | Umschalter |
| `object` | JSON-Editor |
| `array` | JSON-Array-Editor |
| `files` | Datei-Upload |
## Zugriffskontrolle
| Modus | Beschreibung |
|------|-------------|
| **Öffentlich** | Jeder mit dem Link kann absenden |
| **Passwort** | Benutzer müssen ein Passwort eingeben |
| **E-Mail-Whitelist** | Nur angegebene E-Mails/Domains können absenden |
Für E-Mail-Whitelist:
- Exakt: `user@example.com`
- Domain: `@example.com` (alle E-Mails von der Domain)
## Einbettung
### Direkter Link
```
https://sim.ai/form/your-identifier
```
### Iframe
```html
<iframe
src="https://sim.ai/form/your-identifier"
width="100%"
height="600"
frameborder="0"
title="Form"
></iframe>
```
## API-Übermittlung
Formulare programmatisch übermitteln:
<Tabs items={['cURL', 'TypeScript']}>
<Tab value="cURL">
```bash
curl -X POST https://sim.ai/api/form/your-identifier \
-H "Content-Type: application/json" \
-d '{
"formData": {
"name": "John Doe",
"email": "john@example.com"
}
}'
```
</Tab>
<Tab value="TypeScript">
```typescript
const response = await fetch('https://sim.ai/api/form/your-identifier', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
formData: {
name: 'John Doe',
email: 'john@example.com'
}
})
});
const result = await response.json();
// { success: true, data: { executionId: '...' } }
```
</Tab>
</Tabs>
### Geschützte Formulare
Für passwortgeschützte Formulare:
```bash
curl -X POST https://sim.ai/api/form/your-identifier \
-H "Content-Type: application/json" \
-d '{ "password": "secret", "formData": { "name": "John" } }'
```
Für E-Mail-geschützte Formulare:
```bash
curl -X POST https://sim.ai/api/form/your-identifier \
-H "Content-Type: application/json" \
-d '{ "email": "allowed@example.com", "formData": { "name": "John" } }'
```
## Fehlerbehebung
**"Keine Eingabefelder konfiguriert"** - Fügen Sie Eingabeformat-Felder zu Ihrem Start-Block hinzu.
**Formular lädt nicht im Iframe** - Überprüfen Sie, ob die CSP Ihrer Website Iframes von `sim.ai` erlaubt.
**Übermittlungen schlagen fehl** - Überprüfen Sie, ob die Kennung korrekt ist und erforderliche Felder ausgefüllt sind.

View File

@@ -41,6 +41,9 @@ Diese Tastenkombinationen wechseln zwischen den Panel-Tabs auf der rechten Seite
| Tastenkombination | Aktion |
|----------|--------|
| `C` | Copilot-Tab fokussieren |
| `T` | Toolbar-Tab fokussieren |
| `E` | Editor-Tab fokussieren |
| `Mod` + `F` | Toolbar-Suche fokussieren |
## Globale Navigation

View File

@@ -17,7 +17,7 @@ MCP-Server gruppieren Ihre Workflow-Tools zusammen. Erstellen und verwalten Sie
<Video src="mcp/mcp-server.mp4" width={700} height={450} />
</div>
1. Navigieren Sie zu **Einstellungen → MCP-Server**
1. Navigieren Sie zu **Einstellungen → Bereitgestellte MCPs**
2. Klicken Sie auf **Server erstellen**
3. Geben Sie einen Namen und eine optionale Beschreibung ein
4. Kopieren Sie die Server-URL zur Verwendung in Ihren MCP-Clients
@@ -79,7 +79,7 @@ Füge deinen API-Key-Header (`X-API-Key`) für authentifizierten Zugriff hinzu,
## Server-Verwaltung
In der Server-Detailansicht unter **Einstellungen → MCP-Server** können Sie:
In der Server-Detailansicht unter **Einstellungen → Bereitgestellte MCPs** können Sie:
- **Tools anzeigen**: Alle Workflows sehen, die einem Server hinzugefügt wurden
- **URL kopieren**: Die Server-URL für MCP-Clients abrufen

View File

@@ -27,7 +27,7 @@ MCP-Server stellen Sammlungen von Tools bereit, die Ihre Agenten nutzen können.
</div>
1. Navigieren Sie zu Ihren Workspace-Einstellungen
2. Gehen Sie zum Abschnitt **MCP-Server**
2. Gehen Sie zum Abschnitt **Bereitgestellte MCPs**
3. Klicken Sie auf **MCP-Server hinzufügen**
4. Geben Sie die Server-Konfigurationsdetails ein
5. Speichern Sie die Konfiguration

View File

@@ -1,24 +0,0 @@
{
"title": "Sim Documentation",
"pages": [
"./introduction/index",
"./getting-started/index",
"./quick-reference/index",
"triggers",
"blocks",
"tools",
"connections",
"mcp",
"copilot",
"skills",
"knowledgebase",
"variables",
"credentials",
"execution",
"permissions",
"self-hosting",
"./enterprise/index",
"./keyboard-shortcuts/index"
],
"defaultOpen": false
}

View File

@@ -1,394 +0,0 @@
---
title: Kurzreferenz
description: Wesentliche Aktionen zum Navigieren und Verwenden des Sim-Workflow-Editors
---
import { Callout } from 'fumadocs-ui/components/callout'
import { ActionImage, ActionVideo } from '@/components/ui/action-media'
Eine schnelle Übersicht für alltägliche Aktionen im Sim-Workflow-Editor. Für Tastaturkürzel siehe [Tastaturkürzel](/keyboard-shortcuts).
<Callout type="info">
**Mod** bezieht sich auf `Cmd` unter macOS und `Ctrl` unter Windows/Linux.
</Callout>
## Arbeitsbereiche
<table>
<thead>
<tr><th>Aktion</th><th>Wie</th><th>Vorschau</th></tr>
</thead>
<tbody>
<tr>
<td>Arbeitsbereich erstellen</td>
<td>Arbeitsbereich-Dropdown anklicken → **Neuer Arbeitsbereich**</td>
<td><ActionVideo src="quick-reference/create-workspace.mp4" alt="Arbeitsbereich erstellen" /></td>
</tr>
<tr>
<td>Arbeitsbereiche wechseln</td>
<td>Arbeitsbereich-Dropdown anklicken → Arbeitsbereich auswählen</td>
<td><ActionVideo src="quick-reference/switch-workspace.mp4" alt="Arbeitsbereiche wechseln" /></td>
</tr>
<tr>
<td>Teammitglieder einladen</td>
<td>Seitenleiste → **Einladen**</td>
<td><ActionVideo src="quick-reference/invite.mp4" alt="Teammitglieder einladen" /></td>
</tr>
<tr>
<td>Arbeitsbereich umbenennen</td>
<td>Rechtsklick auf Arbeitsbereich → **Umbenennen**</td>
<td rowSpan={4}><ActionImage src="/static/quick-reference/workspace-context-menu.png" alt="Arbeitsbereich-Kontextmenü" /></td>
</tr>
<tr>
<td>Arbeitsbereich duplizieren</td>
<td>Rechtsklick auf Arbeitsbereich → **Duplizieren**</td>
</tr>
<tr>
<td>Arbeitsbereich exportieren</td>
<td>Rechtsklick auf Arbeitsbereich → **Exportieren**</td>
</tr>
<tr>
<td>Arbeitsbereich löschen</td>
<td>Rechtsklick auf Arbeitsbereich → **Löschen**</td>
</tr>
</tbody>
</table>
## Workflows
<table>
<thead>
<tr><th>Aktion</th><th>Wie</th><th>Vorschau</th></tr>
</thead>
<tbody>
<tr>
<td>Workflow erstellen</td>
<td>**+**-Schaltfläche in der Seitenleiste anklicken</td>
<td><ActionImage src="/static/quick-reference/create-workflow.png" alt="Workflow erstellen" /></td>
</tr>
<tr>
<td>Workflows neu anordnen / verschieben</td>
<td>Workflow nach oben/unten oder auf einen Ordner ziehen</td>
<td><ActionVideo src="quick-reference/reordering.mp4" alt="Workflows neu anordnen" /></td>
</tr>
<tr>
<td>Workflow importieren</td>
<td>Import-Schaltfläche in der Seitenleiste anklicken → Datei auswählen</td>
<td><ActionImage src="/static/quick-reference/import-workflow.png" alt="Workflow importieren" /></td>
</tr>
<tr>
<td>Mehrere Workflows auswählen</td>
<td>`Mod+Click` oder `Shift+Click` Workflows in der Seitenleiste</td>
<td><ActionVideo src="quick-reference/multiselect.mp4" alt="Mehrere Workflows auswählen" /></td>
</tr>
<tr>
<td>In neuem Tab öffnen</td>
<td>Rechtsklick auf Workflow → **In neuem Tab öffnen**</td>
<td rowSpan={6}><ActionImage src="/static/quick-reference/workflow-context-menu.png" alt="Workflow-Kontextmenü" /></td>
</tr>
<tr>
<td>Workflow umbenennen</td>
<td>Rechtsklick auf Workflow → **Umbenennen**</td>
</tr>
<tr>
<td>Workflow-Farbe zuweisen</td>
<td>Rechtsklick auf Workflow → **Farbe ändern**</td>
</tr>
<tr>
<td>Workflow duplizieren</td>
<td>Rechtsklick auf Workflow → **Duplizieren**</td>
</tr>
<tr>
<td>Workflow exportieren</td>
<td>Rechtsklick auf Workflow → **Exportieren**</td>
</tr>
<tr>
<td>Workflow löschen</td>
<td>Rechtsklick auf Workflow → **Löschen**</td>
</tr>
<tr>
<td>Ordner umbenennen</td>
<td>Rechtsklick auf Ordner → **Umbenennen**</td>
<td rowSpan={6}><ActionImage src="/static/quick-reference/folder-context-menu.png" alt="Ordner-Kontextmenü" /></td>
</tr>
<tr>
<td>Workflow in Ordner erstellen</td>
<td>Rechtsklick auf Ordner → **Workflow erstellen**</td>
</tr>
<tr>
<td>Ordner in Ordner erstellen</td>
<td>Rechtsklick auf Ordner → **Ordner erstellen**</td>
</tr>
<tr>
<td>Ordner duplizieren</td>
<td>Rechtsklick auf Ordner → **Duplizieren**</td>
</tr>
<tr>
<td>Ordner exportieren</td>
<td>Rechtsklick auf Ordner → **Exportieren**</td>
</tr>
<tr>
<td>Ordner löschen</td>
<td>Rechtsklick auf Ordner → **Löschen**</td>
</tr>
</tbody>
</table>
## Blöcke
<table>
<thead>
<tr><th>Aktion</th><th>Wie</th><th>Vorschau</th></tr>
</thead>
<tbody>
<tr>
<td>Block hinzufügen</td>
<td>Aus Toolbar-Panel ziehen oder Rechtsklick auf Canvas → **Block hinzufügen**</td>
<td><ActionVideo src="quick-reference/add-block.mp4" alt="Block hinzufügen" /></td>
</tr>
<tr>
<td>Mehrere Blöcke auswählen</td>
<td>`Mod+Click` zusätzliche Blöcke oder Shift-Ziehen für Auswahlrahmen</td>
<td><ActionVideo src="quick-reference/multiselect-blocks.mp4" alt="Mehrere Blöcke auswählen" /></td>
</tr>
<tr>
<td>Blöcke kopieren</td>
<td>`Mod+C` mit ausgewählten Blöcken</td>
<td rowSpan={2}><ActionVideo src="quick-reference/copy-paste.mp4" alt="Blöcke kopieren und einfügen" /></td>
</tr>
<tr>
<td>Blöcke einfügen</td>
<td>`Mod+V` zum Einfügen kopierter Blöcke</td>
</tr>
<tr>
<td>Blöcke duplizieren</td>
<td>Rechtsklick → **Duplizieren**</td>
<td><ActionVideo src="quick-reference/duplicate-block.mp4" alt="Blöcke duplizieren" /></td>
</tr>
<tr>
<td>Blöcke löschen</td>
<td>`Delete` oder `Backspace` Taste oder Rechtsklick → **Löschen**</td>
<td><ActionImage src="/static/quick-reference/delete-block.png" alt="Block löschen" /></td>
</tr>
<tr>
<td>Block umbenennen</td>
<td>Auf Blocknamen im Header klicken oder im Editor-Panel bearbeiten</td>
<td><ActionVideo src="quick-reference/rename-block.mp4" alt="Block umbenennen" /></td>
</tr>
<tr>
<td>Block aktivieren/deaktivieren</td>
<td>Rechtsklick → **Aktivieren/Deaktivieren**</td>
<td><ActionImage src="/static/quick-reference/disable-block.png" alt="Block deaktivieren" /></td>
</tr>
<tr>
<td>Block sperren/entsperren</td>
<td>Über Block hovern → Auf Schloss-Symbol klicken (nur Admin)</td>
<td><ActionImage src="/static/quick-reference/lock-block.png" alt="Block sperren" /></td>
</tr>
<tr>
<td>Handle-Ausrichtung umschalten</td>
<td>Rechtsklick → **Handles umschalten**</td>
<td><ActionVideo src="quick-reference/toggle-handles.mp4" alt="Handle-Ausrichtung umschalten" /></td>
</tr>
<tr>
<td>Block konfigurieren</td>
<td>Block auswählen → Editor-Panel rechts verwenden</td>
<td><ActionVideo src="quick-reference/configure-block.mp4" alt="Block konfigurieren" /></td>
</tr>
</tbody>
</table>
## Verbindungen
<table>
<thead>
<tr><th>Aktion</th><th>Wie</th><th>Vorschau</th></tr>
</thead>
<tbody>
<tr>
<td>Verbindung erstellen</td>
<td>Vom Ausgangs-Handle zum Eingangs-Handle ziehen</td>
<td><ActionVideo src="quick-reference/connect-blocks.mp4" alt="Blöcke verbinden" /></td>
</tr>
<tr>
<td>Verbindung löschen</td>
<td>Auf Kante klicken zum Auswählen → `Delete` Taste</td>
<td><ActionVideo src="quick-reference/delete-connection.mp4" alt="Verbindung löschen" /></td>
</tr>
<tr>
<td>Ausgabe in anderem Block verwenden</td>
<td>Verbindungs-Tag in Eingabefeld ziehen</td>
<td><ActionVideo src="quick-reference/connection-tag.mp4" alt="Verbindungs-Tag verwenden" /></td>
</tr>
</tbody>
</table>
## Panels und Ansichten
<table>
<thead>
<tr><th>Aktion</th><th>Wie</th><th>Vorschau</th></tr>
</thead>
<tbody>
<tr>
<td>Symbolleiste durchsuchen</td>
<td>`Mod+F`</td>
<td><ActionVideo src="quick-reference/search-toolbar.mp4" alt="Symbolleiste durchsuchen" /></td>
</tr>
<tr>
<td>Alles durchsuchen</td>
<td>`Mod+K`</td>
<td><ActionImage src="/static/quick-reference/search-everything.png" alt="Alles durchsuchen" /></td>
</tr>
<tr>
<td>Manuellen Modus umschalten</td>
<td>Klicken Sie auf die Umschalt-Schaltfläche, um zwischen manuell und Selektor zu wechseln</td>
<td><ActionImage src="/static/quick-reference/toggle-manual-mode.png" alt="Manuellen Modus umschalten" /></td>
</tr>
<tr>
<td>Seitenleiste ein-/ausklappen</td>
<td>Klicken Sie auf die Einklappen-Schaltfläche in der Seitenleiste</td>
<td><ActionVideo src="quick-reference/collapse-sidebar.mp4" alt="Seitenleiste einklappen" /></td>
</tr>
</tbody>
</table>
## Ausführen und Testen
<table>
<thead>
<tr><th>Aktion</th><th>Wie</th><th>Vorschau</th></tr>
</thead>
<tbody>
<tr>
<td>Workflow ausführen</td>
<td>Klicken Sie auf die Schaltfläche Workflow ausführen oder `Mod+Enter`</td>
<td><ActionImage src="/static/quick-reference/run-workflow.png" alt="Workflow ausführen" /></td>
</tr>
<tr>
<td>Workflow stoppen</td>
<td>Klicken Sie auf die Stopp-Schaltfläche oder `Mod+Enter` während der Ausführung</td>
<td><ActionImage src="/static/quick-reference/stop-workflow.png" alt="Workflow stoppen" /></td>
</tr>
<tr>
<td>Mit Chat testen</td>
<td>Verwenden Sie das Chat-Panel auf der rechten Seite</td>
<td><ActionImage src="/static/quick-reference/test-chat.png" alt="Mit Chat testen" /></td>
</tr>
<tr>
<td>Ausgabe zum Anzeigen auswählen</td>
<td>Klicken Sie auf das Dropdown-Menü im Chat-Panel → Wählen Sie Block-Ausgabe aus</td>
<td><ActionImage src="/static/quick-reference/output-select.png" alt="Ausgabe zum Anzeigen auswählen" /></td>
</tr>
<tr>
<td>Chat-Verlauf löschen</td>
<td>Klicken Sie auf die Löschen-Schaltfläche im Chat-Panel</td>
<td><ActionImage src="/static/quick-reference/clear-chat.png" alt="Chat-Verlauf löschen" /></td>
</tr>
<tr>
<td>Ab Block ausführen</td>
<td>Bewegen Sie den Mauszeiger über den Block → Klicken Sie auf die Wiedergabe-Schaltfläche oder Rechtsklick → **Ab Block ausführen**</td>
<td><ActionImage src="/static/quick-reference/run-from-block.png" alt="Ab Block ausführen" /></td>
</tr>
<tr>
<td>Bis Block ausführen</td>
<td>Rechtsklick auf Block → **Bis Block ausführen**</td>
<td><ActionImage src="/static/quick-reference/run-until-block.png" alt="Bis Block ausführen" /></td>
</tr>
<tr>
<td>Ausführungsprotokolle anzeigen</td>
<td>Öffnen Sie das Terminal-Panel unten oder `Mod+L`</td>
<td><ActionImage src="/static/quick-reference/terminal.png" alt="Terminal für Ausführungsprotokolle" /></td>
</tr>
<tr>
<td>Protokolle filtern</td>
<td>Klicken Sie auf das Filter-Symbol im Terminal → Filtern Sie nach Block oder Status</td>
<td><ActionImage src="/static/quick-reference/filter-block.png" alt="Protokolle nach Block filtern" /></td>
</tr>
<tr>
<td>Protokolle durchsuchen</td>
<td>Verwenden Sie das Suchfeld im Terminal oder Rechtsklick auf Protokolleintrag → **Suchen**</td>
<td><ActionImage src="/static/quick-reference/terminal-search.png" alt="Protokolle durchsuchen" /></td>
</tr>
<tr>
<td>Protokolleintrag kopieren</td>
<td>Zwischenablage-Symbol oder Rechtsklick auf Protokolleintrag → **Kopieren**</td>
<td><ActionImage src="/static/quick-reference/copy-log.png" alt="Protokolleintrag kopieren" /></td>
</tr>
<tr>
<td>Terminal leeren</td>
<td>Papierkorb-Symbol oder `Mod+D`</td>
<td><ActionImage src="/static/quick-reference/clear-terminal.png" alt="Terminal leeren" /></td>
</tr>
</tbody>
</table>
## Bereitstellung
<table>
<thead>
<tr><th>Aktion</th><th>Wie</th><th>Vorschau</th></tr>
</thead>
<tbody>
<tr>
<td>Workflow bereitstellen</td>
<td>Klicken Sie auf die Schaltfläche **Bereitstellen** im Panel</td>
<td><ActionImage src="/static/quick-reference/deploy.png" alt="Workflow bereitstellen" /></td>
</tr>
<tr>
<td>Bereitstellung aktualisieren</td>
<td>Klicken Sie auf **Aktualisieren**, wenn Änderungen erkannt werden</td>
<td><ActionImage src="/static/quick-reference/update-deployment.png" alt="Bereitstellung aktualisieren" /></td>
</tr>
<tr>
<td>Bereitstellungsstatus anzeigen</td>
<td>Überprüfen Sie die Statusanzeige (Live/Aktualisieren/Bereitstellen) im Tab „Bereitstellen"</td>
<td><ActionImage src="/static/quick-reference/view-deployment.png" alt="Bereitstellungsstatus anzeigen" /></td>
</tr>
<tr>
<td>Bereitstellung zurücksetzen</td>
<td>Greifen Sie auf frühere Versionen im Tab „Bereitstellen" zu → **Zu Live befördern**</td>
<td><ActionImage src="/static/quick-reference/promote-deployment.png" alt="Bereitstellung zu Live befördern" /></td>
</tr>
<tr>
<td>Versionsbeschreibung hinzufügen</td>
<td>Tab „Bereitstellen" → Klicken Sie auf das Beschreibungssymbol → Beschreibung hinzufügen oder generieren</td>
<td><ActionVideo src="quick-reference/deployment-description.mp4" alt="Versionsbeschreibung für Bereitstellung hinzufügen" /></td>
</tr>
<tr>
<td>API-Endpunkt kopieren</td>
<td>Tab „Bereitstellen" → API → API-cURL kopieren</td>
<td><ActionImage src="/static/quick-reference/copy-api.png" alt="API-Endpunkt kopieren" /></td>
</tr>
</tbody>
</table>
## Variablen
<table>
<thead>
<tr><th>Aktion</th><th>Wie</th><th>Vorschau</th></tr>
</thead>
<tbody>
<tr>
<td>Workflow-Variable hinzufügen / bearbeiten / löschen</td>
<td>Panel → Variablen → **Variable hinzufügen**, zum Bearbeiten klicken oder Löschsymbol verwenden</td>
<td><ActionImage src="/static/quick-reference/variables.png" alt="Variablen-Panel" /></td>
</tr>
<tr>
<td>Umgebungsvariable hinzufügen</td>
<td>Einstellungen → **Umgebungsvariablen** → **Hinzufügen**</td>
<td><ActionImage src="/static/quick-reference/add-env-variable.png" alt="Umgebungsvariable hinzufügen" /></td>
</tr>
<tr>
<td>Auf Workflow-Variable verweisen</td>
<td>Verwenden Sie die Syntax `<blockName.itemName>` in Block-Eingaben</td>
<td><ActionImage src="/static/quick-reference/variable-reference.png" alt="Auf Workflow-Variable verweisen" /></td>
</tr>
<tr>
<td>Auf Umgebungsvariable verweisen</td>
<td>Verwenden Sie die Syntax `{{ENV_VAR}}` in Block-Eingaben</td>
<td><ActionImage src="/static/quick-reference/env-variable-reference.png" alt="Auf Umgebungsvariable verweisen" /></td>
</tr>
</tbody>
</table>

View File

@@ -7,10 +7,10 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
Das offizielle Python SDK für Sim ermöglicht es Ihnen, Workflows programmatisch aus Ihren Python-Anwendungen heraus mit dem offiziellen Python SDK auszuführen.
Das offizielle Python SDK für Sim ermöglicht es Ihnen, Workflows programmatisch aus Ihren Python-Anwendungen mithilfe des offiziellen Python SDKs auszuführen.
<Callout type="info">
Das Python SDK unterstützt Python 3.8+ mit Unterstützung für asynchrone Ausführung, automatischer Ratenbegrenzung mit exponentiellem Backoff und Nutzungsverfolgung.
Das Python SDK unterstützt Python 3.8+ mit asynchroner Ausführungsunterstützung, automatischer Ratenbegrenzung mit exponentiellem Backoff und Nutzungsverfolgung.
</Callout>
## Installation
@@ -75,16 +75,16 @@ result = client.execute_workflow(
- `input_data` (dict, optional): Eingabedaten, die an den Workflow übergeben werden
- `timeout` (float, optional): Timeout in Sekunden (Standard: 30.0)
- `stream` (bool, optional): Streaming-Antworten aktivieren (Standard: False)
- `selected_outputs` (list[str], optional): Block-Ausgaben zum Streamen im Format `blockName.attribute` (z. B. `["agent1.content"]`)
- `selected_outputs` (list[str], optional): Block-Ausgaben, die im `blockName.attribute`Format gestreamt werden sollen (z.B. `["agent1.content"]`)
- `async_execution` (bool, optional): Asynchron ausführen (Standard: False)
**Rückgabewert:** `WorkflowExecutionResult | AsyncExecutionResult`
**Rückgabe:** `WorkflowExecutionResult | AsyncExecutionResult`
Wenn `async_execution=True`, wird sofort mit einer Task-ID zum Polling zurückgegeben. Andernfalls wird auf die Fertigstellung gewartet.
Wenn `async_execution=True`, wird sofort mit einer Task-ID zum Abfragen zurückgegeben. Andernfalls wird auf den Abschluss gewartet.
##### get_workflow_status()
Ruft den Status eines Workflows ab (Deployment-Status usw.).
Den Status eines Workflows abrufen (Bereitstellungsstatus usw.).
```python
status = client.get_workflow_status("workflow-id")
@@ -98,7 +98,7 @@ print("Is deployed:", status.is_deployed)
##### validate_workflow()
Überprüft, ob ein Workflow zur Ausführung bereit ist.
Überprüfen, ob ein Workflow für die Ausführung bereit ist.
```python
is_ready = client.validate_workflow("workflow-id")
@@ -114,7 +114,7 @@ if is_ready:
##### get_job_status()
Ruft den Status einer asynchronen Job-Ausführung ab.
Den Status einer asynchronen Job-Ausführung abrufen.
```python
status = client.get_job_status("task-id-from-async-execution")
@@ -131,7 +131,7 @@ if status["status"] == "completed":
**Antwortfelder:**
- `success` (bool): Ob die Anfrage erfolgreich war
- `taskId` (str): Die Task-ID
- `status` (str): Einer von `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'`
- `status` (str): Einer der Werte `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'`
- `metadata` (dict): Enthält `startedAt`, `completedAt` und `duration`
- `output` (any, optional): Die Workflow-Ausgabe (wenn abgeschlossen)
- `error` (any, optional): Fehlerdetails (wenn fehlgeschlagen)
@@ -139,7 +139,7 @@ if status["status"] == "completed":
##### execute_with_retry()
Führt einen Workflow mit automatischer Wiederholung bei Rate-Limit-Fehlern unter Verwendung von exponentiellem Backoff aus.
Einen Workflow mit automatischer Wiederholung bei Ratenbegrenzungsfehlern unter Verwendung von exponentiellem Backoff ausführen.
```python
result = client.execute_with_retry(
@@ -161,13 +161,13 @@ result = client.execute_with_retry(
- `selected_outputs` (list, optional): Block-Ausgaben zum Streamen
- `async_execution` (bool, optional): Asynchron ausführen
- `max_retries` (int, optional): Maximale Anzahl von Wiederholungen (Standard: 3)
- `initial_delay` (float, optional): Anfangsverzögerung in Sekunden (Standard: 1.0)
- `initial_delay` (float, optional): Anfängliche Verzögerung in Sekunden (Standard: 1.0)
- `max_delay` (float, optional): Maximale Verzögerung in Sekunden (Standard: 30.0)
- `backoff_multiplier` (float, optional): Backoff-Multiplikator (Standard: 2.0)
**Rückgabe:** `WorkflowExecutionResult | AsyncExecutionResult`
**Rückgabewert:** `WorkflowExecutionResult | AsyncExecutionResult`
Die Wiederholungslogik verwendet exponentielles Backoff (1s → 2s → 4s → 8s...) mit ±25% Jitter, um Thundering Herd zu verhindern. Wenn die API einen `retry-after`-Header bereitstellt, wird dieser stattdessen verwendet.
Die Wiederholungslogik verwendet exponentielles Backoff (1s → 2s → 4s → 8s...) mit ±25% Jitter, um den Thundering-Herd-Effekt zu vermeiden. Wenn die API einen `retry-after` Header bereitstellt, wird dieser stattdessen verwendet.
##### get_rate_limit_info()
@@ -185,7 +185,7 @@ if rate_limit_info:
##### get_usage_limits()
Ruft aktuelle Nutzungslimits und Kontingentinformationen für Ihr Konto ab.
Ruft aktuelle Nutzungslimits und Kontingentinformationen für dein Konto ab.
```python
limits = client.get_usage_limits()
@@ -320,9 +320,9 @@ class SimStudioError(Exception):
**Häufige Fehlercodes:**
- `UNAUTHORIZED`: Ungültiger API-Schlüssel
- `TIMEOUT`: Zeitüberschreitung der Anfrage
- `RATE_LIMIT_EXCEEDED`: Ratenlimit überschritten
- `USAGE_LIMIT_EXCEEDED`: Nutzungslimit überschritten
- `TIMEOUT`: Zeitüberschreitung bei der Anfrage
- `RATE_LIMIT_EXCEEDED`: Ratengrenze überschritten
- `USAGE_LIMIT_EXCEEDED`: Nutzungsgrenze überschritten
- `EXECUTION_ERROR`: Workflow-Ausführung fehlgeschlagen
## Beispiele
@@ -334,7 +334,7 @@ class SimStudioError(Exception):
Richten Sie den SimStudioClient mit Ihrem API-Schlüssel ein.
</Step>
<Step title="Workflow validieren">
Prüfen Sie, ob der Workflow bereitgestellt und zur Ausführung bereit ist.
Prüfen Sie, ob der Workflow bereitgestellt und für die Ausführung bereit ist.
</Step>
<Step title="Workflow ausführen">
Führen Sie den Workflow mit Ihren Eingabedaten aus.
@@ -386,7 +386,7 @@ Behandeln Sie verschiedene Fehlertypen, die während der Workflow-Ausführung au
from simstudio import SimStudioClient, SimStudioError
import os
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_with_error_handling():
try:
@@ -409,20 +409,11 @@ def execute_with_error_handling():
raise
```
### Verwendung des Context-Managers
### Verwendung des Kontextmanagers
Verwenden Sie den Client als Context-Manager, um die Ressourcenbereinigung automatisch zu handhaben:
Verwenden Sie den Client als Kontextmanager, um die Ressourcenbereinigung automatisch zu handhaben:
```python
from simstudio import SimStudioClient
import os
# Using context manager to automatically close the session
with SimStudioClient(api_key=os.getenv("SIM_API_KEY")) as client:
result = client.execute_workflow("workflow-id")
print("Result:", result)
# Session is automatically closed here
```
---CODE-PLACEHOLDER-ef99d3dd509e04865d5b6b0e0e03d3f8---
### Batch-Workflow-Ausführung
@@ -475,7 +466,7 @@ for result in results:
### Asynchrone Workflow-Ausführung
Führen Sie Workflows asynchron für langwierige Aufgaben aus:
Führen Sie Workflows asynchron für lang laufende Aufgaben aus:
```python
import os
@@ -519,9 +510,9 @@ def execute_async():
execute_async()
```
### Ratenlimitierung und Wiederholung
### Rate-Limiting und Wiederholungsversuche
Behandeln Sie Ratenbegrenzungen automatisch mit exponentiellem Backoff:
Behandle Rate-Limits automatisch mit exponentiellem Backoff:
```python
import os
@@ -558,7 +549,7 @@ execute_with_retry_handling()
### Nutzungsüberwachung
Überwachen Sie die Nutzung und Limits Ihres Kontos:
Überwache deine Kontonutzung und -limits:
```python
import os
@@ -602,13 +593,13 @@ check_usage()
### Streaming-Workflow-Ausführung
Führen Sie Workflows mit Echtzeit-Streaming-Antworten aus:
Führe Workflows mit Echtzeit-Streaming-Antworten aus:
```python
from simstudio import SimStudioClient
import os
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
client = SimStudioClient(api_key=os.getenv("SIM_API_KEY"))
def execute_with_streaming():
"""Execute workflow with streaming enabled."""
@@ -628,7 +619,7 @@ def execute_with_streaming():
execute_with_streaming()
```
Die Streaming-Antwort folgt dem Server-Sent-Events- (SSE-) Format:
Die Streaming-Antwort folgt dem Server-Sent Events (SSE) Format:
```
data: {"blockId":"7b7735b9-19e5-4bd6-818b-46aae2596e9f","chunk":"One"}
@@ -697,9 +688,9 @@ if __name__ == '__main__':
app.run(debug=True)
```
### Umgebungs­konfiguration
### Umgebungskonfiguration
Konfigurieren Sie den Client mit Umgebungsvariablen:
Konfiguriere den Client mit Umgebungsvariablen:
<Tabs items={['Development', 'Production']}>
<Tab value="Development">
@@ -736,27 +727,27 @@ Konfigurieren Sie den Client mit Umgebungsvariablen:
</Tab>
</Tabs>
## Ihren API-Schlüssel erhalten
## API-Schlüssel erhalten
<Steps>
<Step title="Bei Sim anmelden">
Navigieren Sie zu [Sim](https://sim.ai) und melden Sie sich in Ihrem Konto an.
Navigiere zu [Sim](https://sim.ai) und melde dich bei deinem Konto an.
</Step>
<Step title="Workflow öffnen">
Navigieren Sie zu dem Workflow, den Sie programmatisch ausführen möchten.
<Step title="Öffne deinen Workflow">
Navigiere zu dem Workflow, den du programmatisch ausführen möchtest.
</Step>
<Step title="Workflow bereitstellen">
Klicken Sie auf "Bereitstellen", um Ihren Workflow bereitzustellen, falls dies noch nicht geschehen ist.
<Step title="Deploye deinen Workflow">
Klicke auf "Deploy", um deinen Workflow zu deployen, falls dies noch nicht geschehen ist.
</Step>
<Step title="API-Schlüssel erstellen oder auswählen">
Wählen oder erstellen Sie während des Bereitstellungsprozesses einen API-Schlüssel.
<Step title="Erstelle oder wähle einen API-Schlüssel">
Wähle während des Deployment-Prozesses einen API-Schlüssel aus oder erstelle einen neuen.
</Step>
<Step title="API-Schlüssel kopieren">
Kopieren Sie den API-Schlüssel, um ihn in Ihrer Python-Anwendung zu verwenden.
<Step title="Kopiere den API-Schlüssel">
Kopiere den API-Schlüssel zur Verwendung in deiner Python-Anwendung.
</Step>
</Steps>
## Voraussetzungen
## Anforderungen
- Python 3.8+
- requests >= 2.25.0

View File

@@ -10,20 +10,12 @@ Stellen Sie Sim auf Ihrer eigenen Infrastruktur mit Docker oder Kubernetes berei
## Anforderungen
| Ressource | Klein | Standard | Produktion |
|----------|-------|----------|------------|
| CPU | 2 Kerne | 4 Kerne | 8+ Kerne |
| RAM | 12 GB | 16 GB | 32+ GB |
| Speicher | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
| Docker | 20.10+ | 20.10+ | Neueste Version |
**Klein**: Entwicklung, Tests, Einzelnutzer (1-5 Nutzer)
**Standard**: Teams (5-50 Nutzer), moderate Arbeitslasten
**Produktion**: Große Teams (50+ Nutzer), Hochverfügbarkeit, intensive Workflow-Ausführung
<Callout type="info">
Die Ressourcenanforderungen werden durch Workflow-Ausführung (isolated-vm Sandboxing), Dateiverarbeitung (In-Memory-Dokumentenparsing) und Vektoroperationen (pgvector) bestimmt. Arbeitsspeicher ist typischerweise der limitierende Faktor, nicht CPU. Produktionsdaten zeigen, dass die Hauptanwendung durchschnittlich 4-8 GB und bei hoher Last bis zu 12 GB benötigt.
</Callout>
| Ressource | Minimum | Empfohlen |
|----------|---------|-------------|
| CPU | 2 Kerne | 4+ Kerne |
| RAM | 12 GB | 16+ GB |
| Speicher | 20 GB SSD | 50+ GB SSD |
| Docker | 20.10+ | Neueste Version |
## Schnellstart
@@ -56,10 +48,3 @@ docker compose -f docker-compose.prod.yml up -d
| realtime | 3002 | WebSocket-Server |
| db | 5432 | PostgreSQL mit pgvector |
| migrations | - | Datenbank-Migrationen (werden einmal ausgeführt) |
| Komponente | Port | Beschreibung |
|-----------|------|-------------|
| simstudio | 3000 | Hauptanwendung |
| realtime | 3002 | WebSocket-Server |
| db | 5432 | PostgreSQL mit pgvector |
| migrations | - | Datenbankmigrationen (wird einmal ausgeführt) |

View File

@@ -1,134 +0,0 @@
---
title: Agent-Fähigkeiten
---
import { Callout } from 'fumadocs-ui/components/callout'
Agent-Fähigkeiten sind wiederverwendbare Anweisungspakete, die Ihren KI-Agenten spezialisierte Funktionen verleihen. Basierend auf dem offenen [Agent Skills](https://agentskills.io)-Format ermöglichen Fähigkeiten Ihnen, Fachwissen, Arbeitsabläufe und Best Practices zu erfassen, die Agenten bei Bedarf laden können.
## Wie Fähigkeiten funktionieren
Fähigkeiten nutzen **progressive Offenlegung**, um den Kontext des Agenten schlank zu halten:
1. **Entdeckung** — Nur Fähigkeitsnamen und Beschreibungen werden in den System-Prompt des Agenten aufgenommen (~50-100 Token jeweils)
2. **Aktivierung** — Wenn der Agent entscheidet, dass eine Fähigkeit relevant ist, ruft er das `load_skill`-Tool auf, um die vollständigen Anweisungen in den Kontext zu laden
3. **Ausführung** — Der Agent folgt den geladenen Anweisungen, um die Aufgabe zu erledigen
Das bedeutet, Sie können viele Fähigkeiten an einen Agenten anhängen, ohne dessen Kontextfenster aufzublähen. Der Agent lädt nur das, was er benötigt.
## Fähigkeiten erstellen
Gehen Sie zu **Einstellungen** und wählen Sie **Fähigkeiten** im Bereich Tools aus.
![Manage Skills](/static/skills/manage-skills.png)
Klicken Sie auf **Hinzufügen**, um eine neue Fähigkeit mit drei Feldern zu erstellen:
| Feld | Beschreibung |
|-------|-------------|
| **Name** | Eine Kennung im Kebab-Case-Format (z. B. `sql-expert`, `code-reviewer`). Maximal 64 Zeichen. |
| **Beschreibung** | Eine kurze Erklärung, was die Fähigkeit tut und wann sie verwendet werden soll. Dies liest der Agent, um zu entscheiden, ob er die Fähigkeit aktiviert. Maximal 1024 Zeichen. |
| **Inhalt** | Die vollständigen Fähigkeitsanweisungen in Markdown. Diese werden geladen, wenn der Agent die Fähigkeit aktiviert. |
<Callout type="info">
Die Beschreibung ist entscheidend — sie ist das Einzige, was der Agent sieht, bevor er entscheidet, eine Fähigkeit zu laden. Seien Sie spezifisch darüber, wann und warum die Fähigkeit verwendet werden sollte.
</Callout>
### Gute Skill-Inhalte schreiben
Skill-Inhalte folgen denselben Konventionen wie [SKILL.md-Dateien](https://agentskills.io/specification):
```markdown
# SQL Expert
## When to use this skill
Use when the user asks you to write, optimize, or debug SQL queries.
## Instructions
1. Always ask which database engine (PostgreSQL, MySQL, SQLite)
2. Use CTEs over subqueries for readability
3. Add index recommendations when relevant
4. Explain query plans for optimization requests
## Common Patterns
...
```
**Empfohlene Struktur:**
- **Wann verwenden** — Spezifische Auslöser und Szenarien
- **Anweisungen** — Schritt-für-Schritt-Anleitung mit nummerierten Listen
- **Beispiele** — Eingabe-/Ausgabe-Beispiele, die das erwartete Verhalten zeigen
- **Häufige Muster** — Wiederverwendbare Ansätze für häufige Aufgaben
- **Sonderfälle** — Fallstricke und besondere Überlegungen
Halten Sie Skills fokussiert und unter 500 Zeilen. Wenn ein Skill zu groß wird, teilen Sie ihn in mehrere spezialisierte Skills auf.
## Skills zu einem Agenten hinzufügen
Öffnen Sie einen beliebigen **Agent**-Block und finden Sie das **Skills**-Dropdown unterhalb des Tool-Bereichs. Wählen Sie die Skills aus, auf die der Agent Zugriff haben soll.
![Skill hinzufügen](/static/skills/add-skill.png)
Ausgewählte Skills erscheinen als Karten, die Sie anklicken können, um sie zu bearbeiten oder zu entfernen.
### Was zur Laufzeit passiert
Wenn der Workflow ausgeführt wird:
1. Der System-Prompt des Agenten enthält einen `<available_skills>`-Abschnitt, der Name und Beschreibung jedes Skills auflistet
2. Ein `load_skill`-Tool wird automatisch zu den verfügbaren Tools des Agenten hinzugefügt
3. Wenn der Agent feststellt, dass ein Skill für die aktuelle Aufgabe relevant ist, ruft er `load_skill` mit dem Skill-Namen auf
4. Der vollständige Skill-Inhalt wird als Tool-Antwort zurückgegeben und gibt dem Agenten detaillierte Anweisungen
Dies funktioniert über alle unterstützten LLM-Anbieter hinweg — das `load_skill`-Tool verwendet standardmäßiges Tool-Calling, sodass keine anbieterspezifische Konfiguration erforderlich ist.
## Häufige Anwendungsfälle
Skills sind besonders wertvoll, wenn Agenten spezialisiertes Wissen oder mehrstufige Workflows benötigen:
**Domain-Expertise**
- `api-integration-expert` — Best Practices für den Aufruf spezifischer APIs (Authentifizierung, Rate Limiting, Fehlerbehandlung)
- `data-transformation` — ETL-Muster, Datenbereinigung und Validierungsregeln
- `code-reviewer` — Code-Review-Richtlinien spezifisch für die Standards Ihres Teams
**Workflow-Vorlagen**
- `bug-investigation` — Schritt-für-Schritt-Debugging-Methodik (reproduzieren → isolieren → testen → beheben)
- `feature-implementation` — Entwicklungs-Workflow von Anforderungen bis zur Bereitstellung
- `document-generator` — Vorlagen und Formatierungsregeln für technische Dokumentation
**Unternehmensspezifisches Wissen**
- `our-architecture` — Systemarchitekturdiagramme, Service-Abhängigkeiten und Bereitstellungsprozesse
- `style-guide` — Markenrichtlinien, Schreibstil, UI/UX-Muster
- `customer-onboarding` — Standardverfahren und häufige Kundenfragen
**Wann Skills vs. Agentenanweisungen verwendet werden sollten:**
- Verwenden Sie **Skills** für Wissen, das über mehrere Workflows hinweg gilt oder sich häufig ändert
- Verwenden Sie **Agentenanweisungen** für aufgabenspezifischen Kontext, der für einen einzelnen Agenten einzigartig ist
## Best Practices
**Effektive Beschreibungen schreiben**
- **Seien Sie spezifisch und keyword-reich** — Statt "Hilft bei SQL", schreiben Sie "Optimierte SQL-Abfragen für PostgreSQL, MySQL und SQLite schreiben, einschließlich Index-Empfehlungen und Abfrageplan-Analyse"
- **Aktivierungstrigger einbeziehen** — Erwähnen Sie spezifische Wörter oder Phrasen, die den Skill auslösen sollten (z. B. "Verwenden, wenn der Benutzer PDFs, Formulare oder Dokumentenextraktion erwähnt")
- **Unter 200 Wörtern halten** — Agenten scannen Beschreibungen schnell; jedes Wort zählt
**Skill-Umfang und Organisation**
- **Ein Skill pro Domäne** — Ein fokussierter `sql-expert`-Skill funktioniert besser als ein breiter `database-everything`-Skill
- **Auf 5-10 Skills pro Agent begrenzen** — Mehr Skills = mehr Entscheidungsaufwand; klein anfangen und bei Bedarf erweitern
- **Große Skills aufteilen** — Wenn ein Skill 500 Zeilen überschreitet, in fokussierte Sub-Skills aufteilen
**Inhaltsstruktur**
- **Markdown-Formatierung verwenden** — Überschriften, Listen und Code-Blöcke helfen Agenten beim Parsen und Befolgen von Anweisungen
- **Beispiele bereitstellen** — Input/Output-Paare zeigen, damit Agenten das erwartete Verhalten verstehen
- **Explizit über Sonderfälle sein** — Gehen Sie nicht davon aus, dass Agenten spezielle Behandlung ableiten werden
**Testen und Iteration**
- **Aktivierung testen** — Führen Sie Ihren Workflow aus und überprüfen Sie, ob der Agent die Skill lädt, wenn erwartet
- **Auf Fehlalarme prüfen** — Stellen Sie sicher, dass Skills nicht aktiviert werden, wenn sie es nicht sollten
- **Beschreibungen verfeinern** — Wenn eine Skill nicht geladen wird, wenn sie benötigt wird, fügen Sie der Beschreibung weitere Schlüsselwörter hinzu
## Mehr erfahren
- [Agent Skills Spezifikation](https://agentskills.io) — Das offene Format für portable Agent-Skills
- [Beispiel-Skills](https://github.com/anthropics/skills) — Community-Skill-Beispiele durchsuchen
- [Best Practices](https://agentskills.io/what-are-skills) — Effektive Skills schreiben

View File

@@ -1,207 +0,0 @@
---
title: A2A
description: Interagiere mit externen A2A-kompatiblen Agenten
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="a2a"
color="#4151B5"
/>
{/* MANUAL-CONTENT-START:intro */}
Das A2A-Protokoll (Agent-to-Agent) ermöglicht es Sim, mit externen KI-Agenten und Systemen zu interagieren, die A2A-kompatible APIs implementieren. Mit A2A kannst du Sims Automatisierungen und Workflows mit Remote-Agenten verbinden wie LLM-gestützten Bots, Microservices und anderen KI-basierten Tools unter Verwendung eines standardisierten Nachrichtenformats.
Mit den A2A-Tools in Sim kannst du:
- **Nachrichten an externe Agenten senden**: Kommuniziere direkt mit Remote-Agenten und übermittle Prompts, Befehle oder Daten.
- **Antworten empfangen und streamen**: Erhalte strukturierte Antworten, Artefakte oder Echtzeit-Updates vom Agenten, während die Aufgabe fortschreitet.
- **Gespräche oder Aufgaben fortsetzen**: Führe mehrstufige Konversationen oder Workflows fort, indem du auf Aufgaben- und Kontext-IDs verweist.
- **Drittanbieter-KI und Automatisierung integrieren**: Nutze externe A2A-kompatible Dienste als Teil deiner Sim-Workflows.
Diese Funktionen ermöglichen es dir, fortgeschrittene Workflows zu erstellen, die Sims native Fähigkeiten mit der Intelligenz und Automatisierung externer KIs oder benutzerdefinierter Agenten kombinieren. Um A2A-Integrationen zu nutzen, benötigst du die Endpunkt-URL des externen Agenten und, falls erforderlich, einen API-Schlüssel oder Zugangsdaten.
{/* MANUAL-CONTENT-END */}
## Nutzungsanleitung
Verwende das A2A-Protokoll (Agent-to-Agent), um mit externen KI-Agenten zu interagieren.
## Tools
### `a2a_send_message`
Sende eine Nachricht an einen externen A2A-kompatiblen Agenten.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `agentUrl` | string | Ja | Die A2A-Agenten-Endpunkt-URL |
| `message` | string | Ja | Nachricht, die an den Agenten gesendet werden soll |
| `taskId` | string | Nein | Aufgaben-ID zum Fortsetzen einer bestehenden Aufgabe |
| `contextId` | string | Nein | Kontext-ID für Gesprächskontinuität |
| `data` | string | Nein | Strukturierte Daten, die mit der Nachricht einbezogen werden sollen \(JSON-String\) |
| `files` | array | Nein | Dateien, die mit der Nachricht einbezogen werden sollen |
| `apiKey` | string | Nein | API-Schlüssel für die Authentifizierung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `content` | string | Textantwort-Inhalt vom Agenten |
| `taskId` | string | Eindeutige Aufgabenkennung |
| `contextId` | string | Gruppiert zusammenhängende Aufgaben/Nachrichten |
| `state` | string | Aktueller Lebenszyklus-Status \(working, completed, failed, canceled, rejected, input_required, auth_required\) |
| `artifacts` | array | Ausgabe-Artefakte der Aufgabe |
| `history` | array | Gesprächsverlauf \(Message-Array\) |
### `a2a_get_task`
Abfrage des Status einer bestehenden A2A-Aufgabe.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `agentUrl` | string | Ja | Die A2A-Agenten-Endpunkt-URL |
| `taskId` | string | Ja | Abzufragende Aufgaben-ID |
| `apiKey` | string | Nein | API-Schlüssel für die Authentifizierung |
| `historyLength` | number | Nein | Anzahl der einzubeziehenden Verlaufsnachrichten |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `taskId` | string | Eindeutige Aufgabenkennung |
| `contextId` | string | Gruppiert zusammenhängende Aufgaben/Nachrichten |
| `state` | string | Aktueller Lebenszyklus-Status \(working, completed, failed, canceled, rejected, input_required, auth_required\) |
| `artifacts` | array | Ausgabe-Artefakte der Aufgabe |
| `history` | array | Gesprächsverlauf \(Message-Array\) |
### `a2a_cancel_task`
Abbrechen einer laufenden A2A-Aufgabe.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `agentUrl` | string | Ja | Die A2A-Agenten-Endpunkt-URL |
| `taskId` | string | Ja | Abzubrechende Aufgaben-ID |
| `apiKey` | string | Nein | API-Schlüssel für die Authentifizierung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `cancelled` | boolean | Ob die Stornierung erfolgreich war |
| `state` | string | Aktueller Lebenszyklus-Status \(working, completed, failed, canceled, rejected, input_required, auth_required\) |
### `a2a_get_agent_card`
Ruft die Agent Card (Discovery-Dokument) für einen A2A-Agenten ab.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `agentUrl` | string | Ja | Die Endpunkt-URL des A2A-Agenten |
| `apiKey` | string | Nein | API-Schlüssel für die Authentifizierung \(falls erforderlich\) |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `name` | string | Anzeigename des Agenten |
| `description` | string | Zweck/Fähigkeiten des Agenten |
| `url` | string | Service-Endpunkt-URL |
| `provider` | object | Details zur Ersteller-Organisation |
| `capabilities` | object | Feature-Support-Matrix |
| `skills` | array | Verfügbare Operationen |
| `version` | string | Vom Agenten unterstützte A2A-Protokollversion |
| `defaultInputModes` | array | Standard-Eingabe-Inhaltstypen, die vom Agenten akzeptiert werden |
| `defaultOutputModes` | array | Standard-Ausgabe-Inhaltstypen, die vom Agenten produziert werden |
### `a2a_resubscribe`
Stellt die Verbindung zu einem laufenden A2A-Task-Stream nach einer Verbindungsunterbrechung wieder her.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `agentUrl` | string | Ja | Die Endpunkt-URL des A2A-Agenten |
| `taskId` | string | Ja | Task-ID, zu der erneut abonniert werden soll |
| `apiKey` | string | Nein | API-Schlüssel für die Authentifizierung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `taskId` | string | Eindeutige Aufgabenkennung |
| `contextId` | string | Gruppiert zusammenhängende Aufgaben/Nachrichten |
| `state` | string | Aktueller Lebenszyklusstatus \(working, completed, failed, canceled, rejected, input_required, auth_required\) |
| `isRunning` | boolean | Ob die Aufgabe noch läuft |
| `artifacts` | array | Ausgabeartefakte der Aufgabe |
| `history` | array | Gesprächsverlauf \(Message-Array\) |
### `a2a_set_push_notification`
Konfigurieren Sie einen Webhook, um Benachrichtigungen über Aufgabenaktualisierungen zu erhalten.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `agentUrl` | string | Ja | Die A2A-Agent-Endpunkt-URL |
| `taskId` | string | Ja | Aufgaben-ID, für die Benachrichtigungen konfiguriert werden sollen |
| `webhookUrl` | string | Ja | HTTPS-Webhook-URL zum Empfang von Benachrichtigungen |
| `token` | string | Nein | Token zur Webhook-Validierung |
| `apiKey` | string | Nein | API-Schlüssel zur Authentifizierung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `url` | string | HTTPS-Webhook-URL für Benachrichtigungen |
| `token` | string | Authentifizierungstoken zur Webhook-Validierung |
| `success` | boolean | Ob der Vorgang erfolgreich war |
### `a2a_get_push_notification`
Rufen Sie die Push-Benachrichtigungs-Webhook-Konfiguration für eine Aufgabe ab.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `agentUrl` | string | Ja | Die A2A-Agent-Endpunkt-URL |
| `taskId` | string | Ja | Aufgaben-ID, für die die Benachrichtigungskonfiguration abgerufen werden soll |
| `apiKey` | string | Nein | API-Schlüssel zur Authentifizierung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `token` | string | Authentifizierungstoken für Webhook-Validierung |
| `exists` | boolean | Ob die Ressource existiert |
### `a2a_delete_push_notification`
Löscht die Push-Benachrichtigungs-Webhook-Konfiguration für eine Aufgabe.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `agentUrl` | string | Ja | Die A2A-Agent-Endpunkt-URL |
| `taskId` | string | Ja | Aufgaben-ID, für die die Benachrichtigungskonfiguration gelöscht werden soll |
| `pushNotificationConfigId` | string | Nein | Push-Benachrichtigungskonfigurations-ID zum Löschen \(optional - Server kann aus taskId ableiten\) |
| `apiKey` | string | Nein | API-Schlüssel für Authentifizierung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Ob die Operation erfolgreich war |

View File

@@ -193,3 +193,8 @@ Erhalte eine Liste defekter Backlinks, die auf eine Zieldomäne oder URL verweis
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `brokenBacklinks` | array | Liste defekter Backlinks |
## Hinweise
- Kategorie: `tools`
- Typ: `ahrefs`

View File

@@ -123,3 +123,8 @@ Mehrere vorhandene Datensätze in einer Airtable-Tabelle aktualisieren
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `records` | json | Array der aktualisierten Airtable-Datensätze |
## Hinweise
- Kategorie: `tools`
- Typ: `airtable`

View File

@@ -1,63 +0,0 @@
---
title: Airweave
description: Durchsuchen Sie Ihre synchronisierten Datensammlungen
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="airweave"
color="#6366F1"
/>
{/* MANUAL-CONTENT-START:intro */}
[Airweave](https://airweave.ai/) ist eine KI-gestützte semantische Suchplattform, die Ihnen hilft, Wissen über alle Ihre synchronisierten Datenquellen hinweg zu entdecken und abzurufen. Airweave wurde für moderne Teams entwickelt und ermöglicht schnelle, relevante Suchergebnisse mithilfe neuraler, hybrider oder schlüsselwortbasierter Strategien, die auf Ihre Bedürfnisse zugeschnitten sind.
Mit Airweave können Sie:
- **Intelligenter suchen**: Verwenden Sie natürlichsprachliche Abfragen, um Informationen aufzudecken, die in Ihren verbundenen Tools und Datenbanken gespeichert sind
- **Ihre Daten vereinheitlichen**: Greifen Sie nahtlos auf Inhalte aus Quellen wie Code, Dokumenten, Chat, E-Mails, Cloud-Dateien und mehr zu
- **Abruf anpassen**: Wählen Sie zwischen hybriden (semantisch + Schlüsselwort), neuralen oder Schlüsselwort-Suchstrategien für optimale Ergebnisse
- **Recall steigern**: Erweitern Sie Suchanfragen mit KI, um umfassendere Antworten zu finden
- **Ergebnisse mit KI neu ordnen**: Priorisieren Sie die relevantesten Antworten mit leistungsstarken Sprachmodellen
- **Sofortige Antworten erhalten**: Generieren Sie klare, KI-gestützte Antworten, die aus Ihren Daten synthetisiert werden
In Sim ermöglicht die Airweave-Integration Ihren Agenten, alle Daten Ihrer Organisation über ein einziges Tool zu durchsuchen, zusammenzufassen und Erkenntnisse zu extrahieren. Nutzen Sie Airweave, um umfassenden, kontextbezogenen Wissensabruf in Ihren Workflows zu ermöglichen sei es beim Beantworten von Fragen, Erstellen von Zusammenfassungen oder Unterstützen dynamischer Entscheidungsfindung.
{/* MANUAL-CONTENT-END */}
## Nutzungsanweisungen
Durchsuchen Sie Ihre synchronisierten Datenquellen mit Airweave. Unterstützt semantische Suche mit hybriden, neuralen oder schlüsselwortbasierten Abrufstrategien. Optional können KI-gestützte Antworten aus Suchergebnissen generiert werden.
## Tools
### `airweave_search`
Durchsuchen Sie Ihre synchronisierten Datensammlungen mit Airweave. Unterstützt semantische Suche mit hybriden, neuralen oder schlüsselwortbasierten Abrufstrategien. Optional können KI-gestützte Antworten aus Suchergebnissen generiert werden.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Ja | Airweave API-Schlüssel für die Authentifizierung |
| `collectionId` | string | Ja | Die lesbare ID der zu durchsuchenden Sammlung |
| `query` | string | Ja | Der Suchanfragetext |
| `limit` | number | Nein | Maximale Anzahl der zurückzugebenden Ergebnisse \(Standard: 100\) |
| `retrievalStrategy` | string | Nein | Abrufstrategie: hybrid \(Standard\), neural oder keyword |
| `expandQuery` | boolean | Nein | Abfragevariationen generieren, um die Trefferquote zu verbessern |
| `rerank` | boolean | Nein | Ergebnisse für verbesserte Relevanz mithilfe von LLM neu ordnen |
| `generateAnswer` | boolean | Nein | Eine natürlichsprachliche Antwort auf die Abfrage generieren |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `results` | array | Suchergebnisse mit Inhalten, Scores und Metadaten aus Ihren synchronisierten Daten |
| ↳ `entity_id` | string | Eindeutige Kennung für die Suchergebnisentität |
| ↳ `source_name` | string | Name der Datenquelle \(z. B. "GitHub", "Slack"\) |
| ↳ `md_content` | string | Markdown-formatierter Inhalt des Ergebnisses |
| ↳ `score` | number | Relevanz-Score aus der Suche |
| ↳ `metadata` | object | Zusätzliche Metadaten, die mit dem Ergebnis verknüpft sind |
| ↳ `breadcrumbs` | array | Navigationspfad zum Ergebnis innerhalb seiner Quelle |
| ↳ `url` | string | URL zum Originalinhalt |
| `completion` | string | KI-generierte Antwort auf die Abfrage \(wenn generateAnswer aktiviert ist\) |

View File

@@ -81,3 +81,8 @@ Führe einen APIFY-Aktor asynchron mit Polling für lang laufende Aufgaben aus
| `status` | string | Laufstatus \(SUCCEEDED, FAILED, usw.\) |
| `datasetId` | string | Dataset-ID mit Ergebnissen |
| `items` | array | Dataset-Elemente \(falls abgeschlossen\) |
## Hinweise
- Kategorie: `tools`
- Typ: `apify`

View File

@@ -567,3 +567,8 @@ Liste des Teams abrufen
| --------- | ---- | ----------- |
| `email_accounts` | json | Array von Team-E-Mail-Konten, die in Apollo verknüpft sind |
| `metadata` | json | Metadaten einschließlich der Gesamtanzahl von E-Mail-Konten |
## Notizen
- Kategorie: `tools`
- Typ: `apollo`

View File

@@ -82,3 +82,8 @@ Suche nach Artikeln eines bestimmten Autors auf ArXiv.
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `authorPapers` | json | Array von Publikationen, die vom angegebenen Autor verfasst wurden |
## Hinweise
- Kategorie: `tools`
- Typ: `arxiv`

View File

@@ -163,16 +163,3 @@ Einen Kommentar (Story) zu einer Asana-Aufgabe hinzufügen
- Kategorie: `tools`
- Typ: `asana`
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `ts` | string | Zeitstempel der Antwort |
| `gid` | string | Global eindeutige Kennung des Kommentars |
| `text` | string | Textinhalt des Kommentars |
| `created_at` | string | Erstellungszeitstempel des Kommentars |
| `created_by` | object | Details zum Kommentarautor |
| ↳ `gid` | string | Autor-GID |
| ↳ `name` | string | Name des Autors |

View File

@@ -53,3 +53,8 @@ Führt eine Browser-Automatisierungsaufgabe mit BrowserUse aus
| `success` | boolean | Status der Aufgabenfertigstellung |
| `output` | json | Ausgabedaten der Aufgabe |
| `steps` | json | Ausgeführte Schritte |
## Hinweise
- Kategorie: `tools`
- Typ: `browser_use`

View File

@@ -1,785 +0,0 @@
---
title: Cal Com
description: Verwalten Sie Cal.com-Buchungen, Veranstaltungstypen, Zeitpläne und
Verfügbarkeiten
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="calcom"
color="#FFFFFE"
/>
{/* MANUAL-CONTENT-START:intro */}
[Cal.com](https://cal.com/) ist eine flexible und quelloffene Planungsplattform, die es einfach macht, Termine, Buchungen, Veranstaltungstypen und Teamverfügbarkeiten zu verwalten.
Mit Cal.com können Sie:
- **Planung automatisieren**: Ermöglichen Sie Nutzern, Ihre verfügbaren Zeitfenster einzusehen und Meetings automatisch zu buchen, ohne E-Mail-Pingpong.
- **Veranstaltungen verwalten**: Erstellen und passen Sie Veranstaltungstypen, Dauern und Regeln für Einzel- oder Gruppenmeetings an.
- **Kalender integrieren**: Verbinden Sie sich nahtlos mit Google, Outlook, Apple oder anderen Kalenderanbietern, um Doppelbuchungen zu vermeiden.
- **Teilnehmer und Gäste verwalten**: Erfassen Sie Teilnehmerinformationen, verwalten Sie Gäste und versenden Sie Einladungen oder Erinnerungen.
- **Verfügbarkeit steuern**: Definieren Sie individuelle Arbeitszeiten, Pufferzeiten und Storno-/Umbuchungsregeln.
- **Workflows automatisieren**: Lösen Sie benutzerdefinierte Aktionen über Webhooks aus, wenn eine Buchung erstellt, storniert oder umgebucht wird.
In Sim ermöglicht die Cal.com-Integration Ihren Agenten, Meetings zu buchen, Verfügbarkeiten zu prüfen, Veranstaltungstypen zu verwalten und Planungsaufgaben programmatisch zu automatisieren. Dies hilft Agenten, Meetings zu koordinieren, Buchungen im Namen von Nutzern zu versenden, Zeitpläne zu prüfen oder auf Buchungsereignisse zu reagieren alles ohne manuelle Eingriffe. Durch die Verbindung von Sim mit Cal.com erschließen Sie hochautomatisierte und intelligente Planungs-Workflows, die sich nahtlos in Ihre umfassenderen Automatisierungsanforderungen integrieren lassen.
{/* MANUAL-CONTENT-END */}
## Nutzungsanleitung
Integrieren Sie Cal.com in Ihren Workflow. Erstellen und verwalten Sie Buchungen, Veranstaltungstypen, Zeitpläne und prüfen Sie Verfügbarkeitsfenster. Unterstützt das Erstellen, Auflisten, Umbuchen und Stornieren von Buchungen sowie die Verwaltung von Veranstaltungstypen und Zeitplänen. Kann auch Workflows basierend auf Cal.com-Webhook-Ereignissen auslösen (Buchung erstellt, storniert, umgebucht). Verbinden Sie Ihr Cal.com-Konto über OAuth.
## Tools
### `calcom_create_booking`
Eine neue Buchung auf Cal.com erstellen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `eventTypeId` | number | Ja | Die ID des zu buchenden Ereignistyps |
| `start` | string | Ja | Startzeit im UTC ISO 8601-Format \(z. B. 2024-01-15T09:00:00Z\) |
| `attendee` | object | Ja | Teilnehmerinformationsobjekt mit Name, E-Mail, Zeitzone und optionaler Telefonnummer \(zusammengestellt aus einzelnen Teilnehmerfeldern\) |
| `guests` | array | Nein | Array von Gast-E-Mail-Adressen |
| `items` | string | Nein | Gast-E-Mail-Adresse |
| `lengthInMinutes` | number | Nein | Dauer der Buchung in Minuten \(überschreibt die Standardeinstellung des Ereignistyps\) |
| `metadata` | object | Nein | Benutzerdefinierte Metadaten, die an die Buchung angehängt werden |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Details der erstellten Buchung |
| ↳ `eventType` | object | Details des Ereignistyps |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `slug` | string | Ereignistyp-Slug |
| ↳ `attendees` | array | Liste der Teilnehmer |
| ↳ `name` | string | Name des Teilnehmers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Teilnehmers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `timeZone` | string | Zeitzone des Teilnehmers \(IANA-Format\) |
| ↳ `phoneNumber` | string | Telefonnummer des Teilnehmers |
| ↳ `language` | string | Sprachpräferenz des Teilnehmers \(ISO-Code\) |
| ↳ `absent` | boolean | Ob der Teilnehmer abwesend war |
| ↳ `hosts` | array | Liste der Gastgeber |
| ↳ `id` | number | Benutzer-ID des Gastgebers |
| ↳ `name` | string | Anzeigename des Gastgebers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Gastgebers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `username` | string | Cal.com-Benutzername des Gastgebers |
| ↳ `timeZone` | string | Zeitzone des Gastgebers \(IANA-Format\) |
| ↳ `id` | number | Numerische Buchungs-ID |
| ↳ `uid` | string | Eindeutige Kennung für die Buchung |
| ↳ `title` | string | Titel der Buchung |
| ↳ `status` | string | Buchungsstatus \(z. B. akzeptiert, ausstehend, storniert\) |
| ↳ `start` | string | Startzeit im ISO 8601-Format |
| ↳ `end` | string | Endzeit im ISO 8601-Format |
| ↳ `duration` | number | Dauer in Minuten |
| ↳ `eventTypeId` | number | Ereignistyp-ID |
| ↳ `meetingUrl` | string | URL zum Beitritt zum Meeting |
| ↳ `location` | string | Ort der Buchung |
| ↳ `absentHost` | boolean | Ob der Gastgeber abwesend war |
| ↳ `guests` | array | Gast-E-Mail-Adressen |
| ↳ `bookingFieldsResponses` | json | Benutzerdefinierte Buchungsfeldantworten \(dynamische Schlüssel basierend auf der Ereignistyp-Konfiguration\) |
| ↳ `metadata` | json | Benutzerdefinierte Metadaten, die an die Buchung angehängt sind \(dynamische Schlüssel-Wert-Paare\) |
| ↳ `icsUid` | string | ICS-Kalender-UID |
| ↳ `createdAt` | string | Zeitpunkt der Erstellung der Buchung |
### `calcom_get_booking`
Details einer bestimmten Buchung anhand ihrer UID abrufen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `bookingUid` | string | Ja | Eindeutige Kennung \(UID\) der Buchung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Buchungsdetails |
| ↳ `eventType` | object | Details zum Ereignistyp |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `slug` | string | Ereignistyp-Slug |
| ↳ `attendees` | array | Liste der Teilnehmer |
| ↳ `name` | string | Name des Teilnehmers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Teilnehmers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `timeZone` | string | Zeitzone des Teilnehmers \(IANA-Format\) |
| ↳ `phoneNumber` | string | Telefonnummer des Teilnehmers |
| ↳ `language` | string | Sprachpräferenz des Teilnehmers \(ISO-Code\) |
| ↳ `absent` | boolean | Ob der Teilnehmer abwesend war |
| ↳ `hosts` | array | Liste der Gastgeber |
| ↳ `id` | number | Benutzer-ID des Gastgebers |
| ↳ `name` | string | Anzeigename des Gastgebers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Gastgebers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `username` | string | Cal.com-Benutzername des Gastgebers |
| ↳ `timeZone` | string | Zeitzone des Gastgebers \(IANA-Format\) |
| ↳ `id` | number | Numerische Buchungs-ID |
| ↳ `uid` | string | Eindeutige Kennung für die Buchung |
| ↳ `title` | string | Titel der Buchung |
| ↳ `description` | string | Beschreibung der Buchung |
| ↳ `status` | string | Buchungsstatus \(z. B. akzeptiert, ausstehend, storniert\) |
| ↳ `start` | string | Startzeit im ISO-8601-Format |
| ↳ `end` | string | Endzeit im ISO-8601-Format |
| ↳ `duration` | number | Dauer in Minuten |
| ↳ `eventTypeId` | number | Ereignistyp-ID |
| ↳ `meetingUrl` | string | URL zum Beitritt zum Meeting |
| ↳ `location` | string | Ort der Buchung |
| ↳ `absentHost` | boolean | Ob der Gastgeber abwesend war |
| ↳ `guests` | array | E-Mail-Adressen der Gäste |
| ↳ `bookingFieldsResponses` | json | Benutzerdefinierte Buchungsfeld-Antworten \(dynamische Schlüssel basierend auf der Ereignistyp-Konfiguration\) |
| ↳ `metadata` | json | Benutzerdefinierte Metadaten, die der Buchung angehängt sind \(dynamische Schlüssel-Wert-Paare\) |
| ↳ `rating` | number | Buchungsbewertung |
| ↳ `icsUid` | string | ICS-Kalender-UID |
| ↳ `cancellationReason` | string | Grund für die Stornierung, falls storniert |
| ↳ `reschedulingReason` | string | Grund für die Umbuchung, falls umgebucht |
| ↳ `rescheduledFromUid` | string | Ursprüngliche Buchungs-UID, falls diese Buchung umgebucht wurde |
| ↳ `rescheduledToUid` | string | Neue Buchungs-UID nach Umbuchung |
| ↳ `cancelledByEmail` | string | E-Mail der Person, die die Buchung storniert hat |
| ↳ `rescheduledByEmail` | string | E-Mail der Person, die die Buchung umgebucht hat |
| ↳ `createdAt` | string | Zeitpunkt der Erstellung der Buchung |
| ↳ `updatedAt` | string | Zeitpunkt der letzten Aktualisierung der Buchung |
### `calcom_list_bookings`
Alle Buchungen mit optionalem Statusfilter auflisten
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `status` | string | Nein | Buchungen nach Status filtern: upcoming, recurring, past, cancelled oder unconfirmed |
| `take` | number | Nein | Anzahl der zurückzugebenden Buchungen \(Paginierungslimit\) |
| `skip` | number | Nein | Anzahl der zu überspringenden Buchungen \(Paginierungsoffset\) |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | array | Array von Buchungen |
| ↳ `eventType` | object | Details zum Ereignistyp |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `slug` | string | Ereignistyp-Slug |
| ↳ `attendees` | array | Liste der Teilnehmer |
| ↳ `name` | string | Name des Teilnehmers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Teilnehmers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `timeZone` | string | Zeitzone des Teilnehmers \(IANA-Format\) |
| ↳ `phoneNumber` | string | Telefonnummer des Teilnehmers |
| ↳ `language` | string | Sprachpräferenz des Teilnehmers \(ISO-Code\) |
| ↳ `absent` | boolean | Ob der Teilnehmer abwesend war |
| ↳ `hosts` | array | Liste der Gastgeber |
| ↳ `id` | number | Benutzer-ID des Gastgebers |
| ↳ `name` | string | Anzeigename des Gastgebers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Gastgebers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `username` | string | Cal.com-Benutzername des Gastgebers |
| ↳ `timeZone` | string | Zeitzone des Gastgebers \(IANA-Format\) |
| ↳ `id` | number | Numerische Buchungs-ID |
| ↳ `uid` | string | Eindeutige Kennung für die Buchung |
| ↳ `title` | string | Titel der Buchung |
| ↳ `description` | string | Beschreibung der Buchung |
| ↳ `status` | string | Buchungsstatus \(z. B. accepted, pending, cancelled\) |
| ↳ `start` | string | Startzeit im ISO-8601-Format |
| ↳ `end` | string | Endzeit im ISO-8601-Format |
| ↳ `duration` | number | Dauer in Minuten |
| ↳ `eventTypeId` | number | Ereignistyp-ID |
| ↳ `meetingUrl` | string | URL zum Beitritt zum Meeting |
| ↳ `location` | string | Ort der Buchung |
| ↳ `absentHost` | boolean | Ob der Gastgeber abwesend war |
| ↳ `guests` | array | E-Mail-Adressen der Gäste |
| ↳ `bookingFieldsResponses` | json | Antworten auf benutzerdefinierte Buchungsfelder \(dynamische Schlüssel basierend auf der Ereignistyp-Konfiguration\) |
| ↳ `metadata` | json | Benutzerdefinierte Metadaten, die der Buchung zugeordnet sind \(dynamische Schlüssel-Wert-Paare\) |
| ↳ `rating` | number | Buchungsbewertung |
| ↳ `icsUid` | string | ICS-Kalender-UID |
| ↳ `cancellationReason` | string | Grund für die Stornierung, falls storniert |
| ↳ `cancelledByEmail` | string | E-Mail der Person, die die Buchung storniert hat |
| ↳ `reschedulingReason` | string | Grund für die Umbuchung, falls umgebucht |
| ↳ `rescheduledByEmail` | string | E-Mail der Person, die die Buchung umgebucht hat |
| ↳ `rescheduledFromUid` | string | Ursprüngliche Buchungs-UID, falls diese Buchung umgebucht wurde |
| ↳ `rescheduledToUid` | string | Neue Buchungs-UID nach Umbuchung |
| ↳ `createdAt` | string | Zeitpunkt der Erstellung der Buchung |
| ↳ `updatedAt` | string | Zeitpunkt der letzten Aktualisierung der Buchung |
| `pagination` | object | Paginierungs-Metadaten |
| ↳ `totalItems` | number | Gesamtanzahl der Elemente |
| ↳ `remainingItems` | number | Verbleibende Elemente nach der aktuellen Seite |
| ↳ `returnedItems` | number | Anzahl der in dieser Antwort zurückgegebenen Elemente |
| ↳ `itemsPerPage` | number | Elemente pro Seite |
| ↳ `currentPage` | number | Aktuelle Seitennummer |
| ↳ `totalPages` | number | Gesamtanzahl der Seiten |
| ↳ `hasNextPage` | boolean | Ob es eine nächste Seite gibt |
| ↳ `hasPreviousPage` | boolean | Ob es eine vorherige Seite gibt |
### `calcom_cancel_booking`
Eine bestehende Buchung stornieren
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `bookingUid` | string | Ja | Eindeutige Kennung \(UID\) der zu stornierenden Buchung |
| `cancellationReason` | string | Nein | Grund für die Stornierung der Buchung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Details der stornierten Buchung |
| ↳ `eventType` | object | Details des Ereignistyps |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `slug` | string | Ereignistyp-Slug |
| ↳ `attendees` | array | Liste der Teilnehmer |
| ↳ `name` | string | Name des Teilnehmers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Teilnehmers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `timeZone` | string | Zeitzone des Teilnehmers \(IANA-Format\) |
| ↳ `phoneNumber` | string | Telefonnummer des Teilnehmers |
| ↳ `language` | string | Sprachpräferenz des Teilnehmers \(ISO-Code\) |
| ↳ `absent` | boolean | Ob der Teilnehmer abwesend war |
| ↳ `hosts` | array | Liste der Gastgeber |
| ↳ `id` | number | Benutzer-ID des Gastgebers |
| ↳ `name` | string | Anzeigename des Gastgebers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Gastgebers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `username` | string | Cal.com-Benutzername des Gastgebers |
| ↳ `timeZone` | string | Zeitzone des Gastgebers \(IANA-Format\) |
| ↳ `id` | number | Numerische Buchungs-ID |
| ↳ `uid` | string | Eindeutige Kennung für die Buchung |
| ↳ `title` | string | Titel der Buchung |
| ↳ `cancellationReason` | string | Grund für die Stornierung, falls storniert |
| ↳ `cancelledByEmail` | string | E-Mail der Person, die die Buchung storniert hat |
| ↳ `start` | string | Startzeit im ISO-8601-Format |
| ↳ `end` | string | Endzeit im ISO-8601-Format |
| ↳ `duration` | number | Dauer in Minuten |
| ↳ `eventTypeId` | number | Ereignistyp-ID |
| ↳ `location` | string | Ort der Buchung |
| ↳ `metadata` | json | Benutzerdefinierte Metadaten, die der Buchung zugeordnet sind \(dynamische Schlüssel-Wert-Paare\) |
| ↳ `createdAt` | string | Zeitpunkt der Erstellung der Buchung |
| ↳ `status` | string | Buchungsstatus \(sollte storniert sein\) |
### `calcom_reschedule_booking`
Eine bestehende Buchung auf einen neuen Zeitpunkt verschieben
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `bookingUid` | string | Ja | Eindeutige Kennung \(UID\) der zu verschiebenden Buchung |
| `start` | string | Ja | Neue Startzeit im UTC ISO 8601-Format \(z. B. 2024-01-15T09:00:00Z\) |
| `reschedulingReason` | string | Nein | Grund für die Verschiebung der Buchung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Details der verschobenen Buchung |
| ↳ `eventType` | object | Details des Ereignistyps |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `slug` | string | Ereignistyp-Slug |
| ↳ `attendees` | array | Liste der Teilnehmer |
| ↳ `name` | string | Name des Teilnehmers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Teilnehmers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `timeZone` | string | Zeitzone des Teilnehmers \(IANA-Format\) |
| ↳ `phoneNumber` | string | Telefonnummer des Teilnehmers |
| ↳ `language` | string | Sprachpräferenz des Teilnehmers \(ISO-Code\) |
| ↳ `absent` | boolean | Ob der Teilnehmer abwesend war |
| ↳ `hosts` | array | Liste der Gastgeber |
| ↳ `id` | number | Benutzer-ID des Gastgebers |
| ↳ `name` | string | Anzeigename des Gastgebers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Gastgebers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `username` | string | Cal.com-Benutzername des Gastgebers |
| ↳ `timeZone` | string | Zeitzone des Gastgebers \(IANA-Format\) |
| ↳ `id` | number | Numerische Buchungs-ID |
| ↳ `title` | string | Titel der Buchung |
| ↳ `status` | string | Buchungsstatus \(z. B. akzeptiert, ausstehend, storniert\) |
| ↳ `reschedulingReason` | string | Grund für die Verschiebung, falls verschoben |
| ↳ `rescheduledFromUid` | string | Ursprüngliche Buchungs-UID, falls diese Buchung verschoben wurde |
| ↳ `rescheduledByEmail` | string | E-Mail der Person, die die Buchung verschoben hat |
| ↳ `duration` | number | Dauer in Minuten |
| ↳ `eventTypeId` | number | Ereignistyp-ID |
| ↳ `meetingUrl` | string | URL zum Beitreten des Meetings |
| ↳ `location` | string | Ort der Buchung |
| ↳ `guests` | array | E-Mail-Adressen der Gäste |
| ↳ `metadata` | json | Benutzerdefinierte Metadaten, die an die Buchung angehängt sind \(dynamische Schlüssel-Wert-Paare\) |
| ↳ `icsUid` | string | ICS-Kalender-UID |
| ↳ `createdAt` | string | Zeitpunkt der Erstellung der Buchung |
| ↳ `uid` | string | Eindeutige Kennung für die neue Buchung |
| ↳ `start` | string | Neue Startzeit im ISO 8601-Format |
| ↳ `end` | string | Neue Endzeit im ISO 8601-Format |
### `calcom_confirm_booking`
Eine ausstehende Buchung bestätigen, die eine Bestätigung erfordert
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `bookingUid` | string | Ja | Eindeutige Kennung \(UID\) der zu bestätigenden Buchung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Details der bestätigten Buchung |
| ↳ `eventType` | object | Details des Ereignistyps |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `slug` | string | Ereignistyp-Slug |
| ↳ `attendees` | array | Liste der Teilnehmer |
| ↳ `name` | string | Name des Teilnehmers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Teilnehmers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `timeZone` | string | Zeitzone des Teilnehmers \(IANA-Format\) |
| ↳ `phoneNumber` | string | Telefonnummer des Teilnehmers |
| ↳ `language` | string | Sprachpräferenz des Teilnehmers \(ISO-Code\) |
| ↳ `absent` | boolean | Ob der Teilnehmer abwesend war |
| ↳ `hosts` | array | Liste der Gastgeber |
| ↳ `id` | number | Benutzer-ID des Gastgebers |
| ↳ `name` | string | Anzeigename des Gastgebers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Gastgebers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail \(kann von der tatsächlichen E-Mail abweichen\) |
| ↳ `username` | string | Cal.com-Benutzername des Gastgebers |
| ↳ `timeZone` | string | Zeitzone des Gastgebers \(IANA-Format\) |
| ↳ `id` | number | Numerische Buchungs-ID |
| ↳ `uid` | string | Eindeutige Kennung für die Buchung |
| ↳ `title` | string | Titel der Buchung |
| ↳ `start` | string | Startzeit im ISO-8601-Format |
| ↳ `end` | string | Endzeit im ISO-8601-Format |
| ↳ `duration` | number | Dauer in Minuten |
| ↳ `eventTypeId` | number | Ereignistyp-ID |
| ↳ `meetingUrl` | string | URL zum Beitreten des Meetings |
| ↳ `location` | string | Ort der Buchung |
| ↳ `guests` | array | E-Mail-Adressen der Gäste |
| ↳ `metadata` | json | Benutzerdefinierte Metadaten, die der Buchung angehängt sind \(dynamische Schlüssel-Wert-Paare\) |
| ↳ `icsUid` | string | ICS-Kalender-UID |
| ↳ `createdAt` | string | Zeitpunkt der Erstellung der Buchung |
| ↳ `status` | string | Buchungsstatus \(sollte akzeptiert/bestätigt sein\) |
### `calcom_decline_booking`
Eine ausstehende Buchungsanfrage ablehnen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `bookingUid` | string | Ja | Eindeutige Kennung (UID) der abzulehnenden Buchung |
| `reason` | string | Nein | Grund für die Ablehnung der Buchung |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Details der abgelehnten Buchung |
| ↳ `eventType` | object | Details des Ereignistyps |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `slug` | string | Ereignistyp-Slug |
| ↳ `attendees` | array | Liste der Teilnehmer |
| ↳ `name` | string | Name des Teilnehmers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Teilnehmers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail (kann von der tatsächlichen E-Mail abweichen) |
| ↳ `timeZone` | string | Zeitzone des Teilnehmers (IANA-Format) |
| ↳ `phoneNumber` | string | Telefonnummer des Teilnehmers |
| ↳ `language` | string | Sprachpräferenz des Teilnehmers (ISO-Code) |
| ↳ `absent` | boolean | Ob der Teilnehmer abwesend war |
| ↳ `hosts` | array | Liste der Gastgeber |
| ↳ `id` | number | Benutzer-ID des Gastgebers |
| ↳ `name` | string | Anzeigename des Gastgebers |
| ↳ `email` | string | Tatsächliche E-Mail-Adresse des Gastgebers |
| ↳ `displayEmail` | string | Öffentlich angezeigte E-Mail (kann von der tatsächlichen E-Mail abweichen) |
| ↳ `username` | string | Cal.com-Benutzername des Gastgebers |
| ↳ `timeZone` | string | Zeitzone des Gastgebers (IANA-Format) |
| ↳ `id` | number | Numerische Buchungs-ID |
| ↳ `uid` | string | Eindeutige Kennung für die Buchung |
| ↳ `title` | string | Titel der Buchung |
| ↳ `cancellationReason` | string | Grund für die Stornierung, falls storniert |
| ↳ `start` | string | Startzeit im ISO-8601-Format |
| ↳ `end` | string | Endzeit im ISO-8601-Format |
| ↳ `duration` | number | Dauer in Minuten |
| ↳ `eventTypeId` | number | Ereignistyp-ID |
| ↳ `location` | string | Ort der Buchung |
| ↳ `metadata` | json | Benutzerdefinierte Metadaten, die der Buchung angehängt sind (dynamische Schlüssel-Wert-Paare) |
| ↳ `createdAt` | string | Zeitpunkt der Erstellung der Buchung |
| ↳ `status` | string | Buchungsstatus (sollte storniert/abgelehnt sein) |
### `calcom_create_event_type`
Einen neuen Ereignistyp in Cal.com erstellen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `title` | string | Ja | Titel des Ereignistyps |
| `slug` | string | Ja | Eindeutiger Slug für die Ereignistyp-URL |
| `lengthInMinutes` | number | Ja | Dauer des Ereignisses in Minuten |
| `description` | string | Nein | Beschreibung des Ereignistyps |
| `slotInterval` | number | Nein | Intervall zwischen verfügbaren Buchungsslots in Minuten |
| `minimumBookingNotice` | number | Nein | Erforderliche Mindestvorlaufzeit vor der Buchung in Minuten |
| `beforeEventBuffer` | number | Nein | Pufferzeit vor dem Ereignis in Minuten |
| `afterEventBuffer` | number | Nein | Pufferzeit nach dem Ereignis in Minuten |
| `scheduleId` | number | Nein | ID des Zeitplans für die Verfügbarkeit |
| `disableGuests` | boolean | Nein | Ob das Hinzufügen von Gästen zu Buchungen deaktiviert ist |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Details des erstellten Ereignistyps |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `title` | string | Ereignistyp-Titel |
| ↳ `slug` | string | Ereignistyp-Slug |
| ↳ `description` | string | Ereignistyp-Beschreibung |
| ↳ `lengthInMinutes` | number | Dauer in Minuten |
| ↳ `slotInterval` | number | Slot-Intervall in Minuten |
| ↳ `minimumBookingNotice` | number | Mindestvorlaufzeit für Buchung in Minuten |
| ↳ `beforeEventBuffer` | number | Puffer vor Ereignis in Minuten |
| ↳ `afterEventBuffer` | number | Puffer nach Ereignis in Minuten |
| ↳ `scheduleId` | number | Zeitplan-ID |
| ↳ `disableGuests` | boolean | Ob Gäste deaktiviert sind |
| ↳ `createdAt` | string | ISO-Zeitstempel der Erstellung |
| ↳ `updatedAt` | string | ISO-Zeitstempel der letzten Aktualisierung |
### `calcom_get_event_type`
Detaillierte Informationen über einen bestimmten Ereignistyp abrufen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `eventTypeId` | number | Ja | Ereignistyp-ID zum Abrufen |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Details zum Ereignistyp |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `title` | string | Titel des Ereignistyps |
| ↳ `slug` | string | Slug des Ereignistyps |
| ↳ `description` | string | Beschreibung des Ereignistyps |
| ↳ `lengthInMinutes` | number | Dauer in Minuten |
| ↳ `slotInterval` | number | Zeitfensterintervall in Minuten |
| ↳ `minimumBookingNotice` | number | Mindestvorlaufzeit für Buchungen in Minuten |
| ↳ `beforeEventBuffer` | number | Puffer vor dem Ereignis in Minuten |
| ↳ `afterEventBuffer` | number | Puffer nach dem Ereignis in Minuten |
| ↳ `scheduleId` | number | Zeitplan-ID |
| ↳ `disableGuests` | boolean | Ob Gäste deaktiviert sind |
| ↳ `createdAt` | string | ISO-Zeitstempel der Erstellung |
| ↳ `updatedAt` | string | ISO-Zeitstempel der letzten Aktualisierung |
### `calcom_list_event_types`
Eine Liste aller Ereignistypen abrufen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `sortCreatedAt` | string | Nein | Sortierung nach Erstellungsdatum: "asc" oder "desc" |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | array | Array von Ereignistypen |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `title` | string | Ereignistyp-Titel |
| ↳ `slug` | string | Ereignistyp-Slug |
| ↳ `description` | string | Ereignistyp-Beschreibung |
| ↳ `lengthInMinutes` | number | Dauer in Minuten |
| ↳ `slotInterval` | number | Zeitfensterintervall in Minuten |
| ↳ `minimumBookingNotice` | number | Mindestvorlaufzeit für Buchungen in Minuten |
| ↳ `beforeEventBuffer` | number | Pufferzeit vor dem Ereignis in Minuten |
| ↳ `afterEventBuffer` | number | Pufferzeit nach dem Ereignis in Minuten |
| ↳ `scheduleId` | number | Zeitplan-ID |
| ↳ `disableGuests` | boolean | Ob Gäste deaktiviert sind |
| ↳ `createdAt` | string | ISO-Zeitstempel der Erstellung |
| ↳ `updatedAt` | string | ISO-Zeitstempel der letzten Aktualisierung |
### `calcom_update_event_type`
Einen bestehenden Ereignistyp in Cal.com aktualisieren
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `eventTypeId` | number | Ja | Zu aktualisierende Ereignistyp-ID \(z. B. 12345\) |
| `title` | string | Nein | Titel des Ereignistyps |
| `slug` | string | Nein | Eindeutiger Slug für die Ereignistyp-URL |
| `lengthInMinutes` | number | Nein | Dauer des Ereignisses in Minuten |
| `description` | string | Nein | Beschreibung des Ereignistyps |
| `slotInterval` | number | Nein | Intervall zwischen verfügbaren Buchungszeitfenstern in Minuten |
| `minimumBookingNotice` | number | Nein | Erforderliche Mindestvorlaufzeit vor der Buchung in Minuten |
| `beforeEventBuffer` | number | Nein | Pufferzeit vor dem Ereignis in Minuten |
| `afterEventBuffer` | number | Nein | Pufferzeit nach dem Ereignis in Minuten |
| `scheduleId` | number | Nein | ID des Zeitplans für die Verfügbarkeit |
| `disableGuests` | boolean | Nein | Ob das Hinzufügen von Gästen zu Buchungen deaktiviert werden soll |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Aktualisierte Details des Ereignistyps |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `title` | string | Titel des Ereignistyps |
| ↳ `slug` | string | Slug des Ereignistyps |
| ↳ `description` | string | Beschreibung des Ereignistyps |
| ↳ `lengthInMinutes` | number | Dauer in Minuten |
| ↳ `slotInterval` | number | Zeitfensterintervall in Minuten |
| ↳ `minimumBookingNotice` | number | Mindestvorlaufzeit für Buchungen in Minuten |
| ↳ `beforeEventBuffer` | number | Puffer vor dem Ereignis in Minuten |
| ↳ `afterEventBuffer` | number | Puffer nach dem Ereignis in Minuten |
| ↳ `scheduleId` | number | Zeitplan-ID |
| ↳ `disableGuests` | boolean | Ob Gäste deaktiviert sind |
| ↳ `createdAt` | string | ISO-Zeitstempel der Erstellung |
| ↳ `updatedAt` | string | ISO-Zeitstempel der letzten Aktualisierung |
### `calcom_delete_event_type`
Einen Ereignistyp aus Cal.com löschen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `eventTypeId` | number | Ja | Zu löschende Ereignistyp-ID |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Details des gelöschten Ereignistyps |
| ↳ `id` | number | Ereignistyp-ID |
| ↳ `lengthInMinutes` | number | Dauer in Minuten |
| ↳ `title` | string | Titel des Ereignistyps |
| ↳ `slug` | string | Slug des Ereignistyps |
### `calcom_create_schedule`
Einen neuen Verfügbarkeitsplan in Cal.com erstellen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `name` | string | Ja | Name des Plans |
| `timeZone` | string | Ja | Zeitzone für den Plan \(z. B. America/New_York\) |
| `isDefault` | boolean | Ja | Ob dieser Plan als Standard festgelegt werden soll |
| `availability` | array | Nein | Verfügbarkeitsintervalle für den Plan |
| `items` | object | Nein | Verfügbarkeitsintervall |
| `properties` | array | Nein | Wochentage \(Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag, Sonntag\) |
| `days` | array | Nein | Wochentage \(Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag, Sonntag\) |
| `startTime` | string | Nein | Startzeit im Format HH:MM |
| `endTime` | string | Nein | Endzeit im Format HH:MM |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Erstellte Plandaten |
| ↳ `id` | number | Plan-ID |
| ↳ `ownerId` | number | Benutzer-ID des Eigentümers |
| ↳ `name` | string | Planname |
| ↳ `timeZone` | string | Zeitzone \(z. B. America/New_York\) |
| ↳ `isDefault` | boolean | Ob dies der Standardplan ist |
| ↳ `availability` | array | Verfügbarkeitsfenster |
| ↳ `days` | array | Wochentage \(Montag, Dienstag usw.\) |
| ↳ `startTime` | string | Startzeit im Format HH:MM |
| ↳ `endTime` | string | Endzeit im Format HH:MM |
| ↳ `overrides` | array | Datumsspezifische Verfügbarkeitsüberschreibungen |
| ↳ `date` | string | Datum im Format JJJJ-MM-TT |
| ↳ `startTime` | string | Startzeit im Format HH:MM |
| ↳ `endTime` | string | Endzeit im Format HH:MM |
### `calcom_get_schedule`
Einen bestimmten Zeitplan anhand der ID von Cal.com abrufen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `scheduleId` | string | Ja | ID des abzurufenden Zeitplans |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Zeitplandaten |
| ↳ `id` | number | Zeitplan-ID |
| ↳ `ownerId` | number | Benutzer-ID des Eigentümers |
| ↳ `name` | string | Zeitplanname |
| ↳ `timeZone` | string | Zeitzone \(z. B. America/New_York\) |
| ↳ `isDefault` | boolean | Ob dies der Standardzeitplan ist |
| ↳ `availability` | array | Verfügbarkeitsfenster |
| ↳ `days` | array | Wochentage \(Montag, Dienstag usw.\) |
| ↳ `startTime` | string | Startzeit im Format HH:MM |
| ↳ `endTime` | string | Endzeit im Format HH:MM |
| ↳ `overrides` | array | Datumsspezifische Verfügbarkeitsüberschreibungen |
| ↳ `date` | string | Datum im Format JJJJ-MM-TT |
| ↳ `startTime` | string | Startzeit im Format HH:MM |
| ↳ `endTime` | string | Endzeit im Format HH:MM |
### `calcom_list_schedules`
Alle Verfügbarkeitszeitpläne von Cal.com auflisten
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | array | Array von Zeitplanobjekten |
| ↳ `id` | number | Zeitplan-ID |
| ↳ `ownerId` | number | Benutzer-ID des Eigentümers |
| ↳ `name` | string | Zeitplanname |
| ↳ `timeZone` | string | Zeitzone \(z. B. America/New_York\) |
| ↳ `isDefault` | boolean | Ob dies der Standardzeitplan ist |
| ↳ `availability` | array | Verfügbarkeitsfenster |
| ↳ `days` | array | Wochentage \(Montag, Dienstag usw.\) |
| ↳ `startTime` | string | Startzeit im Format HH:MM |
| ↳ `endTime` | string | Endzeit im Format HH:MM |
| ↳ `overrides` | array | Datumsspezifische Verfügbarkeitsüberschreibungen |
| ↳ `date` | string | Datum im Format JJJJ-MM-TT |
| ↳ `startTime` | string | Startzeit im Format HH:MM |
| ↳ `endTime` | string | Endzeit im Format HH:MM |
### `calcom_update_schedule`
Einen bestehenden Zeitplan in Cal.com aktualisieren
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `scheduleId` | string | Ja | ID des zu aktualisierenden Zeitplans |
| `name` | string | Nein | Neuer Name für den Zeitplan |
| `timeZone` | string | Nein | Neue Zeitzone für den Zeitplan \(z. B. America/New_York\) |
| `isDefault` | boolean | Nein | Ob dieser Zeitplan als Standard festgelegt werden soll |
| `availability` | array | Nein | Neue Verfügbarkeitsintervalle für den Zeitplan |
| `items` | object | Nein | Verfügbarkeitsintervall |
| `properties` | array | Nein | Wochentage \(Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag, Sonntag\) |
| `days` | array | Nein | Wochentage \(Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag, Sonntag\) |
| `startTime` | string | Nein | Startzeit im Format HH:MM |
| `endTime` | string | Nein | Endzeit im Format HH:MM |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Aktualisierte Zeitplandaten |
| ↳ `id` | number | Zeitplan-ID |
| ↳ `ownerId` | number | Benutzer-ID des Eigentümers |
| ↳ `name` | string | Zeitplanname |
| ↳ `timeZone` | string | Zeitzone \(z. B. America/New_York\) |
| ↳ `isDefault` | boolean | Ob dies der Standardzeitplan ist |
| ↳ `availability` | array | Verfügbarkeitsfenster |
| ↳ `days` | array | Wochentage \(Montag, Dienstag usw.\) |
| ↳ `startTime` | string | Startzeit im Format HH:MM |
| ↳ `endTime` | string | Endzeit im Format HH:MM |
| ↳ `overrides` | array | Datumsspezifische Verfügbarkeitsüberschreibungen |
| ↳ `date` | string | Datum im Format JJJJ-MM-TT |
| ↳ `startTime` | string | Startzeit im Format HH:MM |
| ↳ `endTime` | string | Endzeit im Format HH:MM |
### `calcom_delete_schedule`
Einen Zeitplan aus Cal.com löschen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `scheduleId` | string | Ja | ID des zu löschenden Zeitplans |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus \(Erfolg oder Fehler\) |
### `calcom_get_default_schedule`
Den Standard-Verfügbarkeitszeitplan aus Cal.com abrufen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | object | Standard-Zeitplandaten |
| ↳ `id` | number | Zeitplan-ID |
| ↳ `ownerId` | number | Benutzer-ID des Eigentümers |
| ↳ `name` | string | Zeitplanname |
| ↳ `timeZone` | string | Zeitzone \(z. B. America/New_York\) |
| ↳ `isDefault` | boolean | Ob dies der Standardzeitplan ist |
| ↳ `availability` | array | Verfügbarkeitsfenster |
| ↳ `days` | array | Wochentage \(Montag, Dienstag usw.\) |
| ↳ `startTime` | string | Startzeit im Format HH:MM |
| ↳ `endTime` | string | Endzeit im Format HH:MM |
| ↳ `overrides` | array | Datumsspezifische Verfügbarkeitsüberschreibungen |
| ↳ `date` | string | Datum im Format JJJJ-MM-TT |
| ↳ `startTime` | string | Startzeit im Format HH:MM |
| ↳ `endTime` | string | Endzeit im Format HH:MM |
### `calcom_get_slots`
Verfügbare Buchungsslots für einen Cal.com-Ereignistyp innerhalb eines Zeitraums abrufen
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `start` | string | Ja | Beginn des Zeitraums im UTC ISO 8601-Format \(z. B. 2024-01-15T00:00:00Z\) |
| `end` | string | Ja | Ende des Zeitraums im UTC ISO 8601-Format \(z. B. 2024-01-22T00:00:00Z\) |
| `eventTypeId` | number | Nein | Ereignistyp-ID für direkte Suche |
| `eventTypeSlug` | string | Nein | Ereignistyp-Slug \(erfordert, dass der Benutzername gesetzt ist\) |
| `username` | string | Nein | Benutzername für persönliche Ereignistypen \(erforderlich bei Verwendung von eventTypeSlug\) |
| `timeZone` | string | Nein | Zeitzone für zurückgegebene Slots \(Standard ist UTC\) |
| `duration` | number | Nein | Slot-Länge in Minuten |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Antwortstatus |
| `data` | json | Verfügbare Zeitslots gruppiert nach Datum \(Schlüssel im Format JJJJ-MM-TT\). Jedes Datum ist einem Array von Slot-Objekten mit Startzeit, optionaler Endzeit und Informationen zu Sitzplatz-Events zugeordnet. |

View File

@@ -165,3 +165,8 @@ Ein geplantes Ereignis stornieren
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `resource` | object | Stornierungsdetails |
## Hinweise
- Kategorie: `tools`
- Typ: `calendly`

View File

@@ -52,3 +52,8 @@ Egal, ob Sie sofortige Zusammenfassungen verteilen, Aufgaben protokollieren oder
## Nutzungsanleitung
Erhalten Sie Meeting-Notizen, Aufgaben, Transkripte und Aufzeichnungen, wenn Meetings verarbeitet werden. Circleback nutzt Webhooks, um Daten an Ihre Workflows zu übermitteln.
## Hinweise
- Kategorie: `triggers`
- Typ: `circleback`

Some files were not shown because too many files have changed in this diff Show More