mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
Compare commits
122 Commits
improvemen
...
fix/mother
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34283b551f | ||
|
|
e024f22aa8 | ||
|
|
8837f14194 | ||
|
|
f077751ce8 | ||
|
|
75bdf46e6b | ||
|
|
952915abfc | ||
|
|
cbc9f4248c | ||
|
|
5ba3118495 | ||
|
|
00ff21ab9c | ||
|
|
a2f8ed06c8 | ||
|
|
f347e3fca0 | ||
|
|
e13f52fea2 | ||
|
|
e6b2b739cf | ||
|
|
9ae656c0d5 | ||
|
|
c738226c06 | ||
|
|
8f15be23a0 | ||
|
|
b2d146ca0a | ||
|
|
d06aa1de7e | ||
|
|
5b9f0d73c2 | ||
|
|
7e740e617b | ||
|
|
92290029f0 | ||
|
|
d84cba6d19 | ||
|
|
d90f828e88 | ||
|
|
a8bbab2d21 | ||
|
|
72bb7e6945 | ||
|
|
4cb0f4a2b0 | ||
|
|
fdd587d6af | ||
|
|
e7b4da2689 | ||
|
|
aa0101c666 | ||
|
|
c939f8a76e | ||
|
|
0b19ad0013 | ||
|
|
3d5141d852 | ||
|
|
75832ca007 | ||
|
|
97f78c60b4 | ||
|
|
9295499405 | ||
|
|
6bcbd15ee6 | ||
|
|
68d207df94 | ||
|
|
d5502d602b | ||
|
|
37d524bb0a | ||
|
|
19ef526886 | ||
|
|
ff2a1527ab | ||
|
|
2e1c639a81 | ||
|
|
635179d696 | ||
|
|
f88926a6a8 | ||
|
|
690b47a0bf | ||
|
|
158d5236bc | ||
|
|
1ba1bc8edb | ||
|
|
53fd92a30a | ||
|
|
0a52b09deb | ||
|
|
1d36b80172 | ||
|
|
e6a5e7f4e4 | ||
|
|
a71304200e | ||
|
|
a4d581c76f | ||
|
|
f1efc598d1 | ||
|
|
244cf4ff7e | ||
|
|
ae887185a1 | ||
|
|
06c88441f8 | ||
|
|
127968d467 | ||
|
|
2722f0efbf | ||
|
|
4f45f705a5 | ||
|
|
d640fa0852 | ||
|
|
28f8e0fd97 | ||
|
|
cc38ecaf12 | ||
|
|
0a6a2ee694 | ||
|
|
8579beb199 | ||
|
|
115b4581a5 | ||
|
|
fcdcaed00d | ||
|
|
04fa31864b | ||
|
|
6b355e9b54 | ||
|
|
127994f077 | ||
|
|
efc1aeed70 | ||
|
|
46065983f6 | ||
|
|
2c79d0249f | ||
|
|
1cf7fdfc8c | ||
|
|
37bdffeda0 | ||
|
|
6fa4b9b410 | ||
|
|
f0ee492ada | ||
|
|
a8e0203a92 | ||
|
|
ebb9a2bdd3 | ||
|
|
61a447aba5 | ||
|
|
e91ab6260a | ||
|
|
afaa361801 | ||
|
|
cd88706ea4 | ||
|
|
79bb4e5ad8 | ||
|
|
ee20e119de | ||
|
|
3788660366 | ||
|
|
9be75e3633 | ||
|
|
40bab7731a | ||
|
|
96096e0ad1 | ||
|
|
647a3eb05b | ||
|
|
0195a4cd18 | ||
|
|
b42f80e8ab | ||
|
|
38ac86c4fd | ||
|
|
4cfe8be75a | ||
|
|
49db3ca50b | ||
|
|
e3ff595a84 | ||
|
|
b3424e2047 | ||
|
|
71ecf6c82e | ||
|
|
e9e5ba2c5b | ||
|
|
9233d4ebc9 | ||
|
|
78901ef517 | ||
|
|
47fef540cc | ||
|
|
f193e9ebbc | ||
|
|
c0f22d7722 | ||
|
|
bf0e25c9d0 | ||
|
|
d4f8ac8107 | ||
|
|
63fa938dd7 | ||
|
|
50b882a3ad | ||
|
|
c8a0b62a9c | ||
|
|
4ccb57371b | ||
|
|
c6e147e56a | ||
|
|
345a95f48d | ||
|
|
e07963f88c | ||
|
|
25c59e3e2e | ||
|
|
dde098e8e5 | ||
|
|
5ae0115444 | ||
|
|
fbafe204e5 | ||
|
|
ba7d6ff298 | ||
|
|
40016e79a1 | ||
|
|
e4fb8b2fdd | ||
|
|
d98545d554 | ||
|
|
fadbad4085 |
@@ -20,6 +20,7 @@ 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
|
||||
@@ -115,12 +116,17 @@ export const {ServiceName}Block: BlockConfig = {
|
||||
id: 'credential',
|
||||
title: 'Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: '{service}', // Must match OAuth provider
|
||||
serviceId: '{service}', // Must match OAuth provider service key
|
||||
requiredScopes: getScopesForService('{service}'), // Import from @/lib/oauth/utils
|
||||
placeholder: 'Select account',
|
||||
required: true,
|
||||
}
|
||||
```
|
||||
|
||||
**Scopes:** Always use `getScopesForService(serviceId)` from `@/lib/oauth/utils` for `requiredScopes`. Never hardcode scope arrays — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`.
|
||||
|
||||
**Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`.
|
||||
|
||||
### Selectors (with dynamic options)
|
||||
```typescript
|
||||
// Channel selector (Slack, Discord, etc.)
|
||||
@@ -532,6 +538,41 @@ 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):
|
||||
@@ -589,6 +630,7 @@ 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',
|
||||
@@ -619,6 +661,7 @@ export const ServiceBlock: BlockConfig = {
|
||||
title: 'Service Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'service',
|
||||
requiredScopes: getScopesForService('service'),
|
||||
placeholder: 'Select account',
|
||||
required: true,
|
||||
},
|
||||
@@ -695,16 +738,88 @@ 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`
|
||||
- [ ] Tools.access lists all tool IDs
|
||||
- [ ] Tools.config.tool returns correct tool ID
|
||||
- [ ] 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)
|
||||
- [ ] 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
|
||||
|
||||
437
.claude/commands/add-connector.md
Normal file
437
.claude/commands/add-connector.md
Normal file
@@ -0,0 +1,437 @@
|
||||
---
|
||||
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.
|
||||
|
||||
Three field types are supported: `short-input`, `dropdown`, and `selector`.
|
||||
|
||||
```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' },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Selectors (Canonical Pairs)
|
||||
|
||||
Use `type: 'selector'` to fetch options dynamically from the existing selector registry (`hooks/selectors/registry.ts`). Selectors are always paired with a manual fallback input using the **canonical pair** pattern — a `selector` field (basic mode) and a `short-input` field (advanced mode) linked by `canonicalParamId`.
|
||||
|
||||
The user sees a toggle button (ArrowLeftRight) to switch between the selector dropdown and manual text input. On submit, the modal resolves each canonical pair to the active mode's value, keyed by `canonicalParamId`.
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Every selector field MUST have a canonical pair** — a corresponding `short-input` (or `dropdown`) field with the same `canonicalParamId` and `mode: 'advanced'`.
|
||||
2. **`required` must be set identically on both fields** in a pair. If the selector is required, the manual input must also be required.
|
||||
3. **`canonicalParamId` must match the key the connector expects in `sourceConfig`** (e.g. `baseId`, `channel`, `teamId`). The advanced field's `id` should typically match `canonicalParamId`.
|
||||
4. **`dependsOn` references the selector field's `id`**, not the `canonicalParamId`. The modal propagates dependency clearing across canonical siblings automatically — changing either field in a parent pair clears dependent children.
|
||||
|
||||
### Selector canonical pair example (Airtable base → table cascade)
|
||||
|
||||
```typescript
|
||||
configFields: [
|
||||
// Base: selector (basic) + manual (advanced)
|
||||
{
|
||||
id: 'baseSelector',
|
||||
title: 'Base',
|
||||
type: 'selector',
|
||||
selectorKey: 'airtable.bases', // Must exist in hooks/selectors/registry.ts
|
||||
canonicalParamId: 'baseId',
|
||||
mode: 'basic',
|
||||
placeholder: 'Select a base',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'baseId',
|
||||
title: 'Base ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'baseId',
|
||||
mode: 'advanced',
|
||||
placeholder: 'e.g. appXXXXXXXXXXXXXX',
|
||||
required: true,
|
||||
},
|
||||
// Table: selector depends on base (basic) + manual (advanced)
|
||||
{
|
||||
id: 'tableSelector',
|
||||
title: 'Table',
|
||||
type: 'selector',
|
||||
selectorKey: 'airtable.tables',
|
||||
canonicalParamId: 'tableIdOrName',
|
||||
mode: 'basic',
|
||||
dependsOn: ['baseSelector'], // References the selector field ID
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'tableIdOrName',
|
||||
title: 'Table Name or ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'tableIdOrName',
|
||||
mode: 'advanced',
|
||||
placeholder: 'e.g. Tasks',
|
||||
required: true,
|
||||
},
|
||||
// Non-selector fields stay as-is
|
||||
{ id: 'maxRecords', title: 'Max Records', type: 'short-input', ... },
|
||||
]
|
||||
```
|
||||
|
||||
### Selector with domain dependency (Jira/Confluence pattern)
|
||||
|
||||
When a selector depends on a plain `short-input` field (no canonical pair), `dependsOn` references that field's `id` directly. The `domain` field's value maps to `SelectorContext.domain` automatically via `SELECTOR_CONTEXT_FIELDS`.
|
||||
|
||||
```typescript
|
||||
configFields: [
|
||||
{
|
||||
id: 'domain',
|
||||
title: 'Jira Domain',
|
||||
type: 'short-input',
|
||||
placeholder: 'yoursite.atlassian.net',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'projectSelector',
|
||||
title: 'Project',
|
||||
type: 'selector',
|
||||
selectorKey: 'jira.projects',
|
||||
canonicalParamId: 'projectKey',
|
||||
mode: 'basic',
|
||||
dependsOn: ['domain'],
|
||||
placeholder: 'Select a project',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'projectKey',
|
||||
title: 'Project Key',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'projectKey',
|
||||
mode: 'advanced',
|
||||
placeholder: 'e.g. ENG, PROJ',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### How `dependsOn` maps to `SelectorContext`
|
||||
|
||||
The connector selector field builds a `SelectorContext` from dependency values. For the mapping to work, each dependency's `canonicalParamId` (or field `id` for non-canonical fields) must exist in `SELECTOR_CONTEXT_FIELDS` (`lib/workflows/subblocks/context.ts`):
|
||||
|
||||
```
|
||||
oauthCredential, domain, teamId, projectId, knowledgeBaseId, planId,
|
||||
siteId, collectionId, spreadsheetId, fileId, baseId, datasetId, serviceDeskId
|
||||
```
|
||||
|
||||
### Available selector keys
|
||||
|
||||
Check `hooks/selectors/types.ts` for the full `SelectorKey` union. Common ones for connectors:
|
||||
|
||||
| SelectorKey | Context Deps | Returns |
|
||||
|-------------|-------------|---------|
|
||||
| `airtable.bases` | credential | Base ID + name |
|
||||
| `airtable.tables` | credential, `baseId` | Table ID + name |
|
||||
| `slack.channels` | credential | Channel ID + name |
|
||||
| `gmail.labels` | credential | Label ID + name |
|
||||
| `google.calendar` | credential | Calendar ID + name |
|
||||
| `linear.teams` | credential | Team ID + name |
|
||||
| `linear.projects` | credential, `teamId` | Project ID + name |
|
||||
| `jira.projects` | credential, `domain` | Project key + name |
|
||||
| `confluence.spaces` | credential, `domain` | Space key + name |
|
||||
| `notion.databases` | credential | Database ID + name |
|
||||
| `asana.workspaces` | credential | Workspace GID + name |
|
||||
| `microsoft.teams` | credential | Team ID + name |
|
||||
| `microsoft.channels` | credential, `teamId` | Channel ID + name |
|
||||
| `webflow.sites` | credential | Site ID + name |
|
||||
| `outlook.folders` | credential | Folder ID + name |
|
||||
|
||||
## 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
|
||||
- [ ] **Selector fields configured correctly (if applicable):**
|
||||
- Every `type: 'selector'` field has a canonical pair (`short-input` or `dropdown` with same `canonicalParamId` and `mode: 'advanced'`)
|
||||
- `required` is identical on both fields in each canonical pair
|
||||
- `selectorKey` exists in `hooks/selectors/registry.ts`
|
||||
- `dependsOn` references selector field IDs (not `canonicalParamId`)
|
||||
- Dependency `canonicalParamId` values exist in `SELECTOR_CONTEXT_FIELDS`
|
||||
- [ ] `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`
|
||||
@@ -102,6 +102,7 @@ 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
|
||||
|
||||
@@ -113,6 +114,7 @@ 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}',
|
||||
@@ -143,6 +145,7 @@ export const {Service}Block: BlockConfig = {
|
||||
title: '{Service} Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: '{service}',
|
||||
requiredScopes: getScopesForService('{service}'),
|
||||
required: true,
|
||||
},
|
||||
// Conditional fields per operation
|
||||
@@ -408,7 +411,7 @@ If creating V2 versions (API-aligned outputs):
|
||||
### Block
|
||||
- [ ] Created `blocks/blocks/{service}.ts`
|
||||
- [ ] Defined operation dropdown with all operations
|
||||
- [ ] Added credential field (oauth-input or short-input)
|
||||
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
|
||||
- [ ] Added conditional fields per operation
|
||||
- [ ] Set up dependsOn for cascading selectors
|
||||
- [ ] Configured tools.access with all tool IDs
|
||||
@@ -418,6 +421,12 @@ 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`
|
||||
@@ -436,6 +445,12 @@ 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:
|
||||
@@ -685,13 +700,61 @@ return NextResponse.json({
|
||||
| `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
|
||||
|
||||
1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration
|
||||
2. **Tool IDs are snake_case** - `stripe_create_payment`, not `stripeCreatePayment`
|
||||
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
|
||||
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`
|
||||
|
||||
@@ -147,9 +147,18 @@ closedAt: {
|
||||
},
|
||||
```
|
||||
|
||||
### Nested Properties
|
||||
For complex outputs, define nested structure:
|
||||
### 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:
|
||||
|
||||
```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',
|
||||
@@ -159,7 +168,10 @@ metadata: {
|
||||
count: { type: 'number', description: 'Total count' },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
For arrays of objects, define the item structure:
|
||||
```typescript
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'List of items',
|
||||
@@ -173,6 +185,8 @@ items: {
|
||||
},
|
||||
```
|
||||
|
||||
Only use bare `type: 'json'` without `properties` when the shape is truly dynamic or unknown.
|
||||
|
||||
## Critical Rules for transformResponse
|
||||
|
||||
### Handle Nullable Fields
|
||||
@@ -272,8 +286,13 @@ 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`
|
||||
@@ -281,4 +300,22 @@ If creating V2 tools (API-aligned outputs), use `_v2` suffix:
|
||||
- [ ] No raw JSON dumps in outputs
|
||||
- [ ] Types file has all interfaces
|
||||
- [ ] Index.ts exports all tools
|
||||
- [ ] Tool IDs use snake_case
|
||||
|
||||
## 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)
|
||||
|
||||
289
.claude/commands/validate-integration.md
Normal file
289
.claude/commands/validate-integration.md
Normal file
@@ -0,0 +1,289 @@
|
||||
---
|
||||
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
|
||||
@@ -8,51 +8,210 @@ paths:
|
||||
|
||||
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 { databaseMock, loggerMock } from '@sim/testing'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@sim/db', () => databaseMock)
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
|
||||
import { myFunction } from '@/lib/feature'
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
describe('myFunction', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it.concurrent('isolated tests run in parallel', () => { ... })
|
||||
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 mocks.
|
||||
Always prefer over local test data.
|
||||
|
||||
| Category | Utilities |
|
||||
|----------|-----------|
|
||||
| **Mocks** | `loggerMock`, `databaseMock`, `setupGlobalFetchMock()` |
|
||||
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutorContext()` |
|
||||
| **Mocks** | `loggerMock`, `databaseMock`, `drizzleOrmMock`, `setupGlobalFetchMock()` |
|
||||
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
|
||||
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
|
||||
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
|
||||
| **Requests** | `createMockRequest()`, `createEnvMock()` |
|
||||
|
||||
## Rules
|
||||
## Rules Summary
|
||||
|
||||
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' })
|
||||
```
|
||||
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()`
|
||||
|
||||
26
.cursor/rules/landing-seo-geo.mdc
Normal file
26
.cursor/rules/landing-seo-geo.mdc
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
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.
|
||||
@@ -5,62 +5,122 @@ globs: ["apps/sim/hooks/queries/**/*.ts"]
|
||||
|
||||
# React Query Patterns
|
||||
|
||||
All React Query hooks live in `hooks/queries/`.
|
||||
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 query file defines a keys factory:
|
||||
Every query file defines a hierarchical keys factory with an `all` root key and intermediate plural keys for prefix-level invalidation:
|
||||
|
||||
```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,
|
||||
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,
|
||||
}
|
||||
```
|
||||
|
||||
Never use inline query keys — always use the factory.
|
||||
|
||||
## File Structure
|
||||
|
||||
```typescript
|
||||
// 1. Query keys factory
|
||||
// 2. Types (if needed)
|
||||
// 3. Private fetch functions
|
||||
// 3. Private fetch functions (accept signal parameter)
|
||||
// 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: () => fetchEntities(workspaceId as string),
|
||||
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
|
||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
placeholderData: keepPreviousData, // OK: workspaceId varies
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 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.all }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 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`
|
||||
- **Fetch functions**: `fetchEntity` (private)
|
||||
- **Mutation hooks**: `useCreateEntity`, `useUpdateEntity`, `useDeleteEntity`
|
||||
- **Fetch functions**: `fetchEntity`, `fetchEntities` (private)
|
||||
|
||||
@@ -7,51 +7,210 @@ 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 { databaseMock, loggerMock } from '@sim/testing'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@sim/db', () => databaseMock)
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
|
||||
import { myFunction } from '@/lib/feature'
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
describe('myFunction', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it.concurrent('isolated tests run in parallel', () => { ... })
|
||||
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 mocks.
|
||||
Always prefer over local test data.
|
||||
|
||||
| Category | Utilities |
|
||||
|----------|-----------|
|
||||
| **Mocks** | `loggerMock`, `databaseMock`, `setupGlobalFetchMock()` |
|
||||
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutorContext()` |
|
||||
| **Mocks** | `loggerMock`, `databaseMock`, `drizzleOrmMock`, `setupGlobalFetchMock()` |
|
||||
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
|
||||
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
|
||||
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
|
||||
| **Requests** | `createMockRequest()`, `createEnvMock()` |
|
||||
|
||||
## Rules
|
||||
## Rules Summary
|
||||
|
||||
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' })
|
||||
```
|
||||
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()`
|
||||
|
||||
296
.cursor/skills/add-hosted-key/SKILL.md
Normal file
296
.cursor/skills/add-hosted-key/SKILL.md
Normal file
@@ -0,0 +1,296 @@
|
||||
---
|
||||
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
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM oven/bun:1.3.9-alpine
|
||||
FROM oven/bun:1.3.10-alpine
|
||||
|
||||
# Install necessary packages for development
|
||||
RUN apk add --no-cache \
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/docs-embeddings.yml
vendored
2
.github/workflows/docs-embeddings.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.9
|
||||
bun-version: 1.3.10
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
4
.github/workflows/i18n.yml
vendored
4
.github/workflows/i18n.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.9
|
||||
bun-version: 1.3.10
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.9
|
||||
bun-version: 1.3.10
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
|
||||
2
.github/workflows/migrations.yml
vendored
2
.github/workflows/migrations.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.9
|
||||
bun-version: 1.3.10
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
|
||||
2
.github/workflows/publish-cli.yml
vendored
2
.github/workflows/publish-cli.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.9
|
||||
bun-version: 1.3.10
|
||||
|
||||
- name: Setup Node.js for npm publishing
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/publish-ts-sdk.yml
vendored
2
.github/workflows/publish-ts-sdk.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.9
|
||||
bun-version: 1.3.10
|
||||
|
||||
- name: Setup Node.js for npm publishing
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
30
.github/workflows/test-build.yml
vendored
30
.github/workflows/test-build.yml
vendored
@@ -10,7 +10,7 @@ permissions:
|
||||
jobs:
|
||||
test-build:
|
||||
name: Test and Build
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.9
|
||||
bun-version: 1.3.10
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -38,6 +38,20 @@ 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
|
||||
|
||||
@@ -76,6 +90,16 @@ 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
|
||||
|
||||
@@ -85,6 +109,7 @@ 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
|
||||
@@ -110,6 +135,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
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -26,6 +26,9 @@ bun-debug.log*
|
||||
**/standalone/
|
||||
sim-standalone.tar.gz
|
||||
|
||||
# redis
|
||||
dump.rdb
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
95
CLAUDE.md
95
CLAUDE.md
@@ -134,21 +134,64 @@ Use `devtools` middleware. Use `persist` only when data should survive reload wi
|
||||
|
||||
## React Query
|
||||
|
||||
All React Query hooks live in `hooks/queries/`.
|
||||
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:
|
||||
|
||||
```typescript
|
||||
export const entityKeys = {
|
||||
all: ['entity'] as const,
|
||||
list: (workspaceId?: string) => [...entityKeys.all, 'list', workspaceId ?? ''] 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,
|
||||
}
|
||||
```
|
||||
|
||||
### 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: () => fetchEntities(workspaceId as string),
|
||||
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
|
||||
enabled: Boolean(workspaceId),
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
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) })
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -167,27 +210,51 @@ Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA
|
||||
|
||||
## Testing
|
||||
|
||||
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
|
||||
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
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { databaseMock, loggerMock } from '@sim/testing'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@sim/db', () => databaseMock)
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
|
||||
import { myFunction } from '@/lib/feature'
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
describe('feature', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
it.concurrent('runs in parallel', () => { ... })
|
||||
import { GET } from '@/app/api/my-route/route'
|
||||
|
||||
describe('my route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
|
||||
})
|
||||
it('returns data', async () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
Use `@sim/testing` mocks/factories over local test data. See `.cursor/rules/sim-testing.mdc` for details.
|
||||
### 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.
|
||||
|
||||
## Utils Rules
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">Build and deploy AI agent workflows in minutes.</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">
|
||||
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-6F3DFA" alt="Sim.ai"></a>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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'
|
||||
@@ -12,28 +15,75 @@ 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 page = source.getPage(params.slug, params.lang)
|
||||
const { lang, slug } = resolveLangAndSlug(params)
|
||||
const page = source.getPage(slug, lang)
|
||||
if (!page) notFound()
|
||||
|
||||
const data = page.data as PageData
|
||||
const MDX = data.body
|
||||
const baseUrl = 'https://docs.sim.ai'
|
||||
const markdownContent = await data.getText('processed')
|
||||
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 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 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 generateBreadcrumbs = () => {
|
||||
const breadcrumbs: Array<{ name: string; url: string }> = [
|
||||
{
|
||||
name: 'Home',
|
||||
url: baseUrl,
|
||||
url: BASE_URL,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -41,7 +91,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
|
||||
let currentPath = ''
|
||||
|
||||
urlParts.forEach((part, index) => {
|
||||
if (index === 0 && ['en', 'es', 'fr', 'de', 'ja', 'zh'].includes(part)) {
|
||||
if (index === 0 && SUPPORTED_LANGUAGES.has(part)) {
|
||||
currentPath = `/${part}`
|
||||
return
|
||||
}
|
||||
@@ -56,12 +106,12 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
|
||||
if (index === urlParts.length - 1) {
|
||||
breadcrumbs.push({
|
||||
name: data.title,
|
||||
url: `${baseUrl}${page.url}`,
|
||||
url: `${BASE_URL}${page.url}`,
|
||||
})
|
||||
} else {
|
||||
breadcrumbs.push({
|
||||
name: name,
|
||||
url: `${baseUrl}${currentPath}`,
|
||||
url: `${BASE_URL}${currentPath}`,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -73,7 +123,6 @@ 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
|
||||
@@ -100,10 +149,8 @@ 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'
|
||||
@@ -169,13 +216,70 @@ 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={`${baseUrl}${page.url}`}
|
||||
lang={params.lang}
|
||||
url={`${BASE_URL}${page.url}`}
|
||||
lang={lang}
|
||||
breadcrumb={breadcrumbs}
|
||||
/>
|
||||
<DocsPage
|
||||
@@ -252,27 +356,29 @@ export async function generateMetadata(props: {
|
||||
params: Promise<{ slug?: string[]; lang: string }>
|
||||
}) {
|
||||
const params = await props.params
|
||||
const page = source.getPage(params.slug, params.lang)
|
||||
const { lang, slug } = resolveLangAndSlug(params)
|
||||
const page = source.getPage(slug, lang)
|
||||
if (!page) notFound()
|
||||
|
||||
const data = page.data as PageData
|
||||
const baseUrl = 'https://docs.sim.ai'
|
||||
const fullUrl = `${baseUrl}${page.url}`
|
||||
const data = page.data as unknown as PageData
|
||||
const fullUrl = `${BASE_URL}${page.url}`
|
||||
|
||||
const ogImageUrl = `${baseUrl}/api/og?title=${encodeURIComponent(data.title)}`
|
||||
const ogImageUrl = `${BASE_URL}/api/og?title=${encodeURIComponent(data.title)}`
|
||||
|
||||
return {
|
||||
title: data.title,
|
||||
description:
|
||||
data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
data.description ||
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
|
||||
keywords: [
|
||||
'AI workflow builder',
|
||||
'visual workflow editor',
|
||||
'AI automation',
|
||||
'workflow automation',
|
||||
'AI agents',
|
||||
'no-code AI',
|
||||
'drag and drop workflows',
|
||||
'agentic workforce',
|
||||
'AI agent platform',
|
||||
'agentic workflows',
|
||||
'LLM orchestration',
|
||||
'AI automation',
|
||||
'knowledge base',
|
||||
'AI integrations',
|
||||
data.title?.toLowerCase().split(' '),
|
||||
]
|
||||
.flat()
|
||||
@@ -282,14 +388,15 @@ export async function generateMetadata(props: {
|
||||
openGraph: {
|
||||
title: data.title,
|
||||
description:
|
||||
data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
data.description ||
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
|
||||
url: fullUrl,
|
||||
siteName: 'Sim Documentation',
|
||||
type: 'article',
|
||||
locale: params.lang === 'en' ? 'en_US' : `${params.lang}_${params.lang.toUpperCase()}`,
|
||||
locale: lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`,
|
||||
alternateLocale: ['en', 'es', 'fr', 'de', 'ja', 'zh']
|
||||
.filter((lang) => lang !== params.lang)
|
||||
.map((lang) => (lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`)),
|
||||
.filter((l) => l !== lang)
|
||||
.map((l) => (l === 'en' ? 'en_US' : `${l}_${l.toUpperCase()}`)),
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
@@ -303,7 +410,8 @@ export async function generateMetadata(props: {
|
||||
card: 'summary_large_image',
|
||||
title: data.title,
|
||||
description:
|
||||
data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
data.description ||
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce.',
|
||||
images: [ogImageUrl],
|
||||
creator: '@simdotai',
|
||||
site: '@simdotai',
|
||||
@@ -323,13 +431,13 @@ export async function generateMetadata(props: {
|
||||
alternates: {
|
||||
canonical: fullUrl,
|
||||
languages: {
|
||||
'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}`, '')}`,
|
||||
'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}`, '')}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
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'
|
||||
@@ -55,15 +56,18 @@ 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 } = await params
|
||||
const { lang: rawLang } = await params
|
||||
const lang = SUPPORTED_LANGUAGES.has(rawLang) ? rawLang : 'en'
|
||||
|
||||
const structuredData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: 'Sim Documentation',
|
||||
description:
|
||||
'Comprehensive documentation for Sim - the visual workflow builder for AI Agent Workflows.',
|
||||
'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.',
|
||||
url: 'https://docs.sim.ai',
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
@@ -99,6 +103,7 @@ export default async function Layout({ children, params }: LayoutProps) {
|
||||
</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
|
||||
@@ -107,6 +112,7 @@ export default async function Layout({ children, params }: LayoutProps) {
|
||||
title: <SimLogoFull className='h-7 w-auto' />,
|
||||
}}
|
||||
sidebar={{
|
||||
tabs: false,
|
||||
defaultOpenLevel: 0,
|
||||
collapsible: false,
|
||||
footer: null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
@import "fumadocs-ui/css/neutral.css";
|
||||
@import "fumadocs-ui/css/preset.css";
|
||||
@import "fumadocs-openapi/css/preset.css";
|
||||
|
||||
/* Prevent overscroll bounce effect on the page */
|
||||
html,
|
||||
@@ -8,19 +9,24 @@ body {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
/* Prevent modals/dialogs from shifting layout via scroll-lock compensation */
|
||||
html,
|
||||
body {
|
||||
padding-right: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-fd-primary: #33c482; /* Green from Sim logo */
|
||||
--font-geist-sans: var(--font-geist-sans);
|
||||
--font-geist-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
/* Ensure primary color is set in both light and dark modes */
|
||||
:root {
|
||||
--color-fd-primary: #33c482;
|
||||
--color-fd-primary: var(--color-fd-foreground);
|
||||
}
|
||||
|
||||
/* Match landing page dark background (#1b1b1b) */
|
||||
.dark {
|
||||
--color-fd-primary: #33c482;
|
||||
--color-fd-background: hsl(0, 0%, 10.6%) !important;
|
||||
--color-fd-card: hsl(0, 0%, 13%) !important;
|
||||
--color-fd-popover: hsl(0, 0%, 14%) !important;
|
||||
--color-fd-secondary: hsl(0, 0%, 15.5%) !important;
|
||||
--color-fd-muted: hsl(0, 0%, 18%) !important;
|
||||
}
|
||||
|
||||
/* Font family utilities */
|
||||
@@ -34,16 +40,10 @@ body {
|
||||
"Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
/* Target any potential border classes */
|
||||
* {
|
||||
--fd-border-sidebar: transparent !important;
|
||||
}
|
||||
|
||||
/* Override any CSS custom properties for borders */
|
||||
:root {
|
||||
--fd-border: transparent !important;
|
||||
--fd-border-sidebar: transparent !important;
|
||||
--fd-nav-height: 65px; /* Custom navbar height (h-16 = 64px + 1px border) */
|
||||
--fd-nav-height: 93px; /* Custom navbar height (52px top + 1px divider + 40px tabs) */
|
||||
/* Content container width used to center main content */
|
||||
--spacing-fd-container: 1400px;
|
||||
/* Edge gutter = leftover space on each side of centered container */
|
||||
@@ -59,34 +59,31 @@ body {
|
||||
--content-gap: 1.75rem;
|
||||
}
|
||||
|
||||
/* Light mode navbar and search styling */
|
||||
/* Light mode navbar background */
|
||||
:root:not(.dark) nav {
|
||||
background-color: hsla(0, 0%, 96%, 0.85) !important;
|
||||
}
|
||||
|
||||
:root:not(.dark) nav button[type="button"] {
|
||||
background-color: hsla(0, 0%, 93%, 0.85) !important;
|
||||
/* Dark mode navbar background */
|
||||
:root.dark nav {
|
||||
background-color: hsla(0, 0%, 10.6%, 0.92) !important;
|
||||
}
|
||||
|
||||
:root.dark nav button[type="button"] {
|
||||
background-color: hsla(0, 0%, 15%, 0.85) !important;
|
||||
backdrop-filter: blur(33px) saturate(180%) !important;
|
||||
-webkit-backdrop-filter: blur(33px) saturate(180%) !important;
|
||||
color: rgba(0, 0, 0, 0.6) !important;
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
:root:not(.dark) nav button[type="button"] kbd {
|
||||
color: rgba(0, 0, 0, 0.6) !important;
|
||||
}
|
||||
|
||||
/* Dark mode navbar and search styling */
|
||||
:root.dark nav {
|
||||
background-color: hsla(0, 0%, 7.04%, 0.92) !important;
|
||||
backdrop-filter: blur(25px) saturate(180%) brightness(0.6) !important;
|
||||
-webkit-backdrop-filter: blur(25px) saturate(180%) brightness(0.6) !important;
|
||||
:root.dark nav button[type="button"] kbd {
|
||||
color: rgba(255, 255, 255, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Floating sidebar appearance - remove background */
|
||||
[data-sidebar-container],
|
||||
#nd-sidebar {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
--color-fd-muted: transparent !important;
|
||||
--color-fd-card: transparent !important;
|
||||
@@ -96,9 +93,7 @@ body {
|
||||
aside[data-sidebar],
|
||||
aside#nd-sidebar {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
/* Fumadocs v16: Add sidebar placeholder styling for grid area */
|
||||
@@ -111,7 +106,7 @@ aside#nd-sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Mobile only: Reduce gap between navbar and content */
|
||||
/* Mobile only: Reduce gap between navbar and content (custom navbar hidden on mobile) */
|
||||
@media (max-width: 1023px) {
|
||||
#nd-docs-layout {
|
||||
margin-top: -25px;
|
||||
@@ -135,11 +130,11 @@ aside#nd-sidebar {
|
||||
/* On mobile, let fumadocs handle the layout natively */
|
||||
@media (min-width: 1024px) {
|
||||
:root {
|
||||
--fd-banner-height: 65px !important; /* 64px navbar + 1px border */
|
||||
--fd-banner-height: 93px !important; /* 52px top + 1px divider + 40px tabs */
|
||||
}
|
||||
|
||||
#nd-docs-layout {
|
||||
--fd-docs-height: calc(100dvh - 65px) !important; /* 64px navbar + 1px border */
|
||||
--fd-docs-height: calc(100dvh - 93px) !important; /* 52px top + 1px divider + 40px tabs */
|
||||
--fd-sidebar-width: 300px !important;
|
||||
margin-left: var(--sidebar-offset) !important;
|
||||
margin-right: var(--toc-offset) !important;
|
||||
@@ -157,7 +152,6 @@ aside#nd-sidebar {
|
||||
#nd-sidebar > div {
|
||||
padding: 0.5rem 12px 12px;
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Override sidebar item styling to match Raindrop */
|
||||
@@ -434,10 +428,6 @@ aside[data-sidebar],
|
||||
#nd-sidebar,
|
||||
#nd-sidebar * {
|
||||
border: none !important;
|
||||
border-right: none !important;
|
||||
border-left: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
/* Override fumadocs background colors for sidebar */
|
||||
@@ -447,7 +437,6 @@ aside[data-sidebar],
|
||||
--color-fd-muted: transparent !important;
|
||||
--color-fd-secondary: transparent !important;
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Force normal text flow in sidebar */
|
||||
@@ -564,16 +553,700 @@ main[data-main] {
|
||||
padding-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
/* Override Fumadocs default content padding */
|
||||
article[data-content],
|
||||
div[data-content] {
|
||||
padding-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
/* Remove any unwanted borders/outlines from video elements */
|
||||
/* Remove any unwanted outlines from video elements */
|
||||
video {
|
||||
outline: none !important;
|
||||
border-style: solid !important;
|
||||
}
|
||||
|
||||
/* API Reference Pages — Mintlify-style overrides */
|
||||
|
||||
/* OpenAPI pages: span main + TOC grid columns for wide two-column layout.
|
||||
Use named grid lines from grid-template-areas so this works regardless
|
||||
of whether the grid has 3 columns (production) or 5 columns (local dev). */
|
||||
#nd-page:has(.api-page-header) {
|
||||
grid-column: main-start / toc-end !important;
|
||||
max-width: 1400px !important;
|
||||
}
|
||||
|
||||
/* Hide the empty TOC aside on OpenAPI pages so it doesn't overlay content */
|
||||
#nd-docs-layout:has(#nd-page:has(.api-page-header)) #nd-toc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide the default "Response Body" heading rendered by fumadocs-openapi */
|
||||
.response-section-wrapper > .response-section-content > h2,
|
||||
.response-section-wrapper > .response-section-content > h3 {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide default accordion triggers (status code rows) — we show our own dropdown */
|
||||
.response-section-wrapper [data-orientation="vertical"] > [data-state] > h3 {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Ensure API reference pages use the same font as the rest of the docs */
|
||||
#nd-page:has(.api-page-header),
|
||||
#nd-page:has(.api-page-header) h2,
|
||||
#nd-page:has(.api-page-header) h3,
|
||||
#nd-page:has(.api-page-header) h4,
|
||||
#nd-page:has(.api-page-header) p,
|
||||
#nd-page:has(.api-page-header) span,
|
||||
#nd-page:has(.api-page-header) div,
|
||||
#nd-page:has(.api-page-header) label,
|
||||
#nd-page:has(.api-page-header) button {
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Method badge pills — shared background colors (page + sidebar) */
|
||||
span.font-mono.font-medium[data-method="get"],
|
||||
span.font-mono.font-medium[data-method="head"],
|
||||
span.font-mono.font-medium[data-method="options"] {
|
||||
background-color: rgb(220 252 231 / 0.85);
|
||||
}
|
||||
html.dark span.font-mono.font-medium[data-method="get"],
|
||||
html.dark span.font-mono.font-medium[data-method="head"],
|
||||
html.dark span.font-mono.font-medium[data-method="options"] {
|
||||
background-color: rgb(34 197 94 / 0.15);
|
||||
}
|
||||
span.font-mono.font-medium[data-method="post"] {
|
||||
background-color: rgb(219 234 254 / 0.85);
|
||||
}
|
||||
html.dark span.font-mono.font-medium[data-method="post"] {
|
||||
background-color: rgb(59 130 246 / 0.15);
|
||||
}
|
||||
span.font-mono.font-medium[data-method="put"] {
|
||||
background-color: rgb(254 249 195 / 0.85);
|
||||
}
|
||||
html.dark span.font-mono.font-medium[data-method="put"] {
|
||||
background-color: rgb(234 179 8 / 0.15);
|
||||
}
|
||||
span.font-mono.font-medium[data-method="patch"] {
|
||||
background-color: rgb(255 237 213 / 0.85);
|
||||
}
|
||||
html.dark span.font-mono.font-medium[data-method="patch"] {
|
||||
background-color: rgb(249 115 22 / 0.15);
|
||||
}
|
||||
span.font-mono.font-medium[data-method="delete"] {
|
||||
background-color: rgb(254 226 226 / 0.85);
|
||||
}
|
||||
html.dark span.font-mono.font-medium[data-method="delete"] {
|
||||
background-color: rgb(239 68 68 / 0.15);
|
||||
}
|
||||
|
||||
/* Sidebar links with method badges — flex for vertical centering */
|
||||
#nd-sidebar a:has(span.font-mono.font-medium) {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* Sidebar method badges — fixed-width for right-aligned labels */
|
||||
#nd-sidebar a span.font-mono.font-medium {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.625rem;
|
||||
font-size: 0.625rem !important;
|
||||
line-height: 1 !important;
|
||||
padding: 0.15625rem 0.25rem;
|
||||
border-radius: 0.1875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Footer navigation method badges — pill styling to match sidebar */
|
||||
#nd-page span.font-mono.font-medium[data-method] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.625rem !important;
|
||||
line-height: 1 !important;
|
||||
padding: 0.15625rem 0.375rem;
|
||||
border-radius: 0.1875rem;
|
||||
}
|
||||
|
||||
/* Code block containers — match regular docs styling */
|
||||
#nd-page:has(.api-page-header) figure.shiki {
|
||||
border-radius: 0.75rem !important;
|
||||
background-color: var(--color-fd-card) !important;
|
||||
}
|
||||
|
||||
/* Hide "Filter Properties" search bar everywhere — main page and popovers */
|
||||
input[placeholder="Filter Properties"] {
|
||||
display: none !important;
|
||||
}
|
||||
div:has(> input[placeholder="Filter Properties"]) {
|
||||
display: none !important;
|
||||
}
|
||||
/* Remove top border on first visible property after hidden Filter Properties */
|
||||
div:has(> input[placeholder="Filter Properties"]) + .text-sm.border-t {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
/* Hide "TypeScript Definitions" copy panel on API pages */
|
||||
#nd-page:has(.api-page-header) div.not-prose.rounded-xl.border.p-3.mb-4 {
|
||||
display: none !important;
|
||||
}
|
||||
#nd-page:has(.api-page-header) div.not-prose.rounded-xl.border.p-3:has(> div > p.font-medium) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide info tags (Format, Default, etc.) everywhere — main page and popovers */
|
||||
div.flex.flex-row.gap-2.flex-wrap.not-prose:has(> div.bg-fd-secondary) {
|
||||
display: none !important;
|
||||
}
|
||||
div.flex.flex-row.items-start.bg-fd-secondary.border.rounded-lg.text-xs {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Method+path bar — cleaner, lighter styling like Gumloop.
|
||||
Override bg-fd-card CSS variable directly for reliability. */
|
||||
#nd-page:has(.api-page-header) div.flex.flex-row.items-center.rounded-xl.border.not-prose {
|
||||
--color-fd-card: rgb(249 250 251) !important;
|
||||
background-color: rgb(249 250 251) !important;
|
||||
border-color: rgb(229 231 235) !important;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose {
|
||||
--color-fd-card: rgb(24 24 27) !important;
|
||||
background-color: rgb(24 24 27) !important;
|
||||
border-color: rgb(63 63 70) !important;
|
||||
}
|
||||
/* Method badge inside path bar — cleaner sans-serif, softer colors */
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium {
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 0.6875rem !important;
|
||||
letter-spacing: 0.025em;
|
||||
text-transform: uppercase;
|
||||
padding: 0.125rem 0.5rem !important;
|
||||
border-radius: 0.375rem !important;
|
||||
}
|
||||
/* Path bar per-method colors (fumadocs renders these, so we match by class) */
|
||||
/* GET */
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium[class*="text-green"] {
|
||||
color: rgb(22 163 74) !important;
|
||||
background-color: rgb(220 252 231 / 0.7) !important;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium[class*="text-green"] {
|
||||
color: rgb(74 222 128) !important;
|
||||
background-color: rgb(34 197 94 / 0.15) !important;
|
||||
}
|
||||
/* POST */
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium[class*="text-blue"] {
|
||||
color: rgb(37 99 235) !important;
|
||||
background-color: rgb(219 234 254 / 0.7) !important;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium[class*="text-blue"] {
|
||||
color: rgb(96 165 250) !important;
|
||||
background-color: rgb(59 130 246 / 0.15) !important;
|
||||
}
|
||||
/* PUT */
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium[class*="text-yellow"] {
|
||||
color: rgb(161 98 7) !important;
|
||||
background-color: rgb(254 249 195 / 0.7) !important;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium[class*="text-yellow"] {
|
||||
color: rgb(250 204 21) !important;
|
||||
background-color: rgb(234 179 8 / 0.15) !important;
|
||||
}
|
||||
/* PATCH */
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium[class*="text-orange"] {
|
||||
color: rgb(194 65 12) !important;
|
||||
background-color: rgb(255 237 213 / 0.7) !important;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium[class*="text-orange"] {
|
||||
color: rgb(251 146 60) !important;
|
||||
background-color: rgb(249 115 22 / 0.15) !important;
|
||||
}
|
||||
/* DELETE */
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium[class*="text-red"] {
|
||||
color: rgb(185 28 28) !important;
|
||||
background-color: rgb(254 226 226 / 0.7) !important;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
span.font-mono.font-medium[class*="text-red"] {
|
||||
color: rgb(248 113 113) !important;
|
||||
background-color: rgb(239 68 68 / 0.15) !important;
|
||||
}
|
||||
|
||||
/* Path text inside method+path bar — monospace, bright like Gumloop */
|
||||
#nd-page:has(.api-page-header) div.flex.flex-row.items-center.rounded-xl.border.not-prose code {
|
||||
color: rgb(55 65 81) !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
font-size: 0.8125rem !important;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
||||
code {
|
||||
color: rgb(229 231 235) !important;
|
||||
}
|
||||
|
||||
/* Inline code in API pages — neutral color instead of red.
|
||||
Exclude code inside the method+path bar (handled above). */
|
||||
#nd-page:has(.api-page-header) .prose :not(pre) > code {
|
||||
color: rgb(79 70 229) !important;
|
||||
}
|
||||
html.dark #nd-page:has(.api-page-header) .prose :not(pre) > code {
|
||||
color: rgb(165 180 252) !important;
|
||||
}
|
||||
|
||||
/* Response Section — custom dropdown-based rendering (Mintlify style) */
|
||||
|
||||
/* Hide divider lines between accordion items */
|
||||
.response-section-wrapper [data-orientation="vertical"].divide-y > * {
|
||||
border-top-width: 0 !important;
|
||||
border-bottom-width: 0 !important;
|
||||
}
|
||||
.response-section-wrapper [data-orientation="vertical"].divide-y {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
/* Remove content type labels inside accordion items (we show one in the header) */
|
||||
.response-section-wrapper [data-orientation="vertical"] p.not-prose:has(code.text-xs) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide the top-level response description (e.g. "Execution was successfully cancelled.")
|
||||
but NOT field descriptions inside Schema which also use prose-no-margin.
|
||||
The response description is a direct child of AccordionContent (role=region) with mb-2. */
|
||||
.response-section-wrapper [data-orientation="vertical"] [role="region"] > .prose-no-margin.mb-2,
|
||||
.response-section-wrapper
|
||||
[data-orientation="vertical"]
|
||||
[role="region"]
|
||||
> div
|
||||
> .prose-no-margin.mb-2 {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Remove left padding on accordion content so it aligns with Path Parameters */
|
||||
.response-section-wrapper [data-orientation="vertical"] [role="region"] {
|
||||
padding-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
/* Response section header */
|
||||
.response-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.response-section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--color-fd-foreground);
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.response-section-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Status code dropdown */
|
||||
.response-section-dropdown-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.response-section-dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.125rem 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-fd-muted-foreground);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s;
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
.response-section-dropdown-trigger:hover {
|
||||
color: var(--color-fd-foreground);
|
||||
}
|
||||
|
||||
.response-section-chevron {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.response-section-chevron-open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.response-section-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.25rem);
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
min-width: 5rem;
|
||||
background-color: white;
|
||||
border: 1px solid rgb(229 231 235);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgb(0 0 0 / 0.1),
|
||||
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
padding: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
html.dark .response-section-dropdown-menu {
|
||||
background-color: rgb(24 24 27);
|
||||
border-color: rgb(63 63 70);
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3);
|
||||
}
|
||||
|
||||
.response-section-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-fd-muted-foreground);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
transition:
|
||||
background-color 0.1s,
|
||||
color 0.1s;
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
.response-section-dropdown-item:hover {
|
||||
background-color: rgb(243 244 246);
|
||||
color: var(--color-fd-foreground);
|
||||
}
|
||||
html.dark .response-section-dropdown-item:hover {
|
||||
background-color: rgb(39 39 42);
|
||||
}
|
||||
.response-section-dropdown-item-selected {
|
||||
color: var(--color-fd-foreground);
|
||||
}
|
||||
|
||||
.response-section-check {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
|
||||
.response-section-content-type {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-fd-muted-foreground);
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Response schema container — remove border to match Path Parameters style */
|
||||
.response-section-wrapper [data-orientation="vertical"] .border.px-3.py-2.rounded-lg {
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Property row — reorder: name (1) → type badge (2) → required badge (3) */
|
||||
#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Name span — order 1 */
|
||||
#nd-page:has(.api-page-header)
|
||||
.flex.flex-wrap.items-center.gap-3.not-prose
|
||||
> span.font-medium.font-mono.text-fd-primary {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
/* Type badge — order 2, grey pill */
|
||||
#nd-page:has(.api-page-header)
|
||||
.flex.flex-wrap.items-center.gap-3.not-prose
|
||||
> span.text-sm.font-mono.text-fd-muted-foreground {
|
||||
order: 2;
|
||||
background-color: rgb(241 245 249);
|
||||
color: rgb(71 85 105);
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.125rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
.flex.flex-wrap.items-center.gap-3.not-prose
|
||||
> span.text-sm.font-mono.text-fd-muted-foreground {
|
||||
background-color: rgb(51 51 56);
|
||||
color: rgb(212 212 220);
|
||||
}
|
||||
|
||||
/* Hide the "*" inside the name span — we'll add "required" as a ::after on the flex row */
|
||||
#nd-page:has(.api-page-header) span.font-medium.font-mono.text-fd-primary > span.text-red-400 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Required badge — order 3, red pill */
|
||||
#nd-page:has(.api-page-header)
|
||||
.flex.flex-wrap.items-center.gap-3.not-prose:has(span.text-red-400)::after {
|
||||
content: "required";
|
||||
order: 3;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: rgb(254 226 226);
|
||||
color: rgb(185 28 28);
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.125rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
.flex.flex-wrap.items-center.gap-3.not-prose:has(span.text-red-400)::after {
|
||||
background-color: rgb(153 27 27 / 0.3);
|
||||
color: rgb(252 165 165);
|
||||
}
|
||||
|
||||
/* Optional "?" indicator — hide it */
|
||||
#nd-page:has(.api-page-header)
|
||||
span.font-medium.font-mono.text-fd-primary
|
||||
> span.text-fd-muted-foreground {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide the auth scheme type label (e.g. "apiKey") next to Authorization heading */
|
||||
#nd-page:has(.api-page-header) .flex.items-start.justify-between.gap-2 > div.not-prose {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Auth property — replace "<token>" with "string" badge, add "header" and "required" badges.
|
||||
Auth properties use my-4 (vs py-4 for regular properties). */
|
||||
|
||||
/* Auth property flex row — name: order 1, type: order 2, ::before "header": order 3, ::after "required": order 4 */
|
||||
#nd-page:has(.api-page-header)
|
||||
div.my-4
|
||||
> .flex.flex-wrap.items-center.gap-3.not-prose
|
||||
> span.font-medium.font-mono.text-fd-primary {
|
||||
order: 1;
|
||||
}
|
||||
#nd-page:has(.api-page-header)
|
||||
div.my-4
|
||||
> .flex.flex-wrap.items-center.gap-3.not-prose
|
||||
> span.text-sm.font-mono.text-fd-muted-foreground {
|
||||
order: 2;
|
||||
font-size: 0;
|
||||
padding: 0 !important;
|
||||
background: none !important;
|
||||
line-height: 0;
|
||||
}
|
||||
#nd-page:has(.api-page-header)
|
||||
div.my-4
|
||||
> .flex.flex-wrap.items-center.gap-3.not-prose
|
||||
> span.text-sm.font-mono.text-fd-muted-foreground::after {
|
||||
content: "string";
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.125rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
background-color: rgb(241 245 249);
|
||||
color: rgb(71 85 105);
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
div.my-4
|
||||
> .flex.flex-wrap.items-center.gap-3.not-prose
|
||||
> span.text-sm.font-mono.text-fd-muted-foreground::after {
|
||||
background-color: rgb(51 51 56);
|
||||
color: rgb(212 212 220);
|
||||
}
|
||||
|
||||
/* "header" badge via ::before on the auth flex row */
|
||||
#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::before {
|
||||
content: "header";
|
||||
order: 3;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: rgb(241 245 249);
|
||||
color: rgb(71 85 105);
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.125rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
div.my-4
|
||||
> .flex.flex-wrap.items-center.gap-3.not-prose::before {
|
||||
background-color: rgb(51 51 56);
|
||||
color: rgb(212 212 220);
|
||||
}
|
||||
|
||||
/* "required" badge via ::after on the auth flex row — red pill */
|
||||
#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::after {
|
||||
content: "required";
|
||||
order: 4;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: rgb(254 226 226);
|
||||
color: rgb(185 28 28);
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.125rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
div.my-4
|
||||
> .flex.flex-wrap.items-center.gap-3.not-prose::after {
|
||||
background-color: rgb(153 27 27 / 0.3);
|
||||
color: rgb(252 165 165);
|
||||
}
|
||||
|
||||
/* Hide "In: header" text below auth property — redundant with the header badge */
|
||||
#nd-page:has(.api-page-header) div.my-4 .prose-no-margin p:has(> code) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Section dividers — bottom border after Authorization and Body sections. */
|
||||
.api-section-divider {
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid rgb(229 231 235 / 0.6);
|
||||
}
|
||||
html.dark .api-section-divider {
|
||||
border-bottom-color: rgb(255 255 255 / 0.07);
|
||||
}
|
||||
|
||||
/* Property rows — breathing room like Mintlify.
|
||||
Regular properties use border-t py-4; auth properties use border-t my-4. */
|
||||
#nd-page:has(.api-page-header) .text-sm.border-t.py-4 {
|
||||
padding-top: 1.25rem !important;
|
||||
padding-bottom: 1.25rem !important;
|
||||
}
|
||||
#nd-page:has(.api-page-header) .text-sm.border-t.my-4 {
|
||||
margin-top: 1.25rem !important;
|
||||
margin-bottom: 1.25rem !important;
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
|
||||
/* Divider lines between fields — very subtle like Mintlify */
|
||||
#nd-page:has(.api-page-header) .text-sm.border-t {
|
||||
border-color: rgb(229 231 235 / 0.6);
|
||||
}
|
||||
html.dark #nd-page:has(.api-page-header) .text-sm.border-t {
|
||||
border-color: rgb(255 255 255 / 0.07);
|
||||
}
|
||||
|
||||
/* Body/Callback section "application/json" label — remove inline code styling */
|
||||
#nd-page:has(.api-page-header) .flex.gap-2.items-center.justify-between p.not-prose code.text-xs,
|
||||
#nd-page:has(.api-page-header) .flex.justify-between.gap-2.items-end p.not-prose code.text-xs {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
color: var(--color-fd-muted-foreground) !important;
|
||||
font-size: 0.875rem !important;
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Object/array type triggers in property rows — order 2 + badge chip styling */
|
||||
#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > button,
|
||||
#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > span:has(> button) {
|
||||
order: 2;
|
||||
background-color: rgb(241 245 249);
|
||||
color: rgb(71 85 105);
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.125rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
html.dark #nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > button,
|
||||
html.dark
|
||||
#nd-page:has(.api-page-header)
|
||||
.flex.flex-wrap.items-center.gap-3.not-prose
|
||||
> span:has(> button) {
|
||||
background-color: rgb(51 51 56);
|
||||
color: rgb(212 212 220);
|
||||
}
|
||||
|
||||
/* Section headings (Authorization, Path Parameters, etc.) — consistent top spacing */
|
||||
#nd-page:has(.api-page-header) .min-w-0.flex-1 h2 {
|
||||
margin-top: 1.75rem !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
/* Code examples in right column — wrap long lines instead of horizontal scroll */
|
||||
#nd-page:has(.api-page-header) pre {
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-all !important;
|
||||
}
|
||||
#nd-page:has(.api-page-header) pre code {
|
||||
width: 100% !important;
|
||||
word-break: break-all !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
|
||||
/* API page header — constrain title/copy-page to left content column, not full width.
|
||||
Only applies on OpenAPI pages (which have the two-column layout). */
|
||||
@media (min-width: 1280px) {
|
||||
.api-page-header {
|
||||
max-width: calc(100% - 400px - 1.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer navigation — constrain to left content column on OpenAPI pages only.
|
||||
Target pages that contain the two-column layout via :has() selector. */
|
||||
#nd-page:has(.api-page-header) > div:last-child {
|
||||
max-width: calc(100% - 400px - 1.5rem);
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
#nd-page:has(.api-page-header) > div:last-child {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tailwind v4 content sources */
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -7,26 +7,27 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
export const metadata = {
|
||||
metadataBase: new URL('https://docs.sim.ai'),
|
||||
title: {
|
||||
default: 'Sim Documentation - Visual Workflow Builder for AI Applications',
|
||||
default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
template: '%s',
|
||||
},
|
||||
description:
|
||||
'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.',
|
||||
'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.',
|
||||
keywords: [
|
||||
'AI workflow builder',
|
||||
'visual workflow editor',
|
||||
'AI automation',
|
||||
'workflow automation',
|
||||
'AI agents',
|
||||
'no-code AI',
|
||||
'drag and drop workflows',
|
||||
'agentic workforce',
|
||||
'AI agent platform',
|
||||
'open-source AI agents',
|
||||
'agentic workflows',
|
||||
'LLM orchestration',
|
||||
'AI integrations',
|
||||
'workflow canvas',
|
||||
'AI Agent Workflow Builder',
|
||||
'workflow orchestration',
|
||||
'agent builder',
|
||||
'AI workflow automation',
|
||||
'visual programming',
|
||||
'knowledge base',
|
||||
'AI automation',
|
||||
'workflow builder',
|
||||
'AI workflow orchestration',
|
||||
'enterprise AI',
|
||||
'AI agent deployment',
|
||||
'intelligent automation',
|
||||
'AI tools',
|
||||
],
|
||||
authors: [{ name: 'Sim Team', url: 'https://sim.ai' }],
|
||||
creator: 'Sim',
|
||||
@@ -53,9 +54,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 - Visual Workflow Builder for AI Applications',
|
||||
title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
description:
|
||||
'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.',
|
||||
'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.',
|
||||
images: [
|
||||
{
|
||||
url: 'https://docs.sim.ai/api/og?title=Sim%20Documentation',
|
||||
@@ -67,9 +68,9 @@ export const metadata = {
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Sim Documentation - Visual Workflow Builder for AI Applications',
|
||||
title: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
description:
|
||||
'Comprehensive documentation for Sim - the visual workflow builder for AI applications.',
|
||||
'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.',
|
||||
creator: '@simdotai',
|
||||
site: '@simdotai',
|
||||
images: ['https://docs.sim.ai/api/og?title=Sim%20Documentation'],
|
||||
|
||||
@@ -37,9 +37,9 @@ export async function GET() {
|
||||
|
||||
const manifest = `# Sim Documentation
|
||||
|
||||
> Visual Workflow Builder for AI Applications
|
||||
> The open-source platform to build AI agents and run your agentic workforce.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Documentation Overview
|
||||
|
||||
|
||||
@@ -52,15 +52,26 @@ 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)
|
||||
const [open, setOpen] = useState(hasActiveChild || (isApiRef && isOnApiRefPage))
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(hasActiveChild)
|
||||
}, [hasActiveChild])
|
||||
setOpen(hasActiveChild || (isApiRef && isOnApiRefPage))
|
||||
}, [hasActiveChild, isApiRef, isOnApiRefPage])
|
||||
|
||||
const active = item.index ? isActive(item.index.url, pathname, false) : false
|
||||
|
||||
@@ -157,16 +168,18 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac
|
||||
{hasChildren && (
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-hidden transition-all duration-200 ease-in-out',
|
||||
open ? 'max-h-[10000px] opacity-100' : 'max-h-0 opacity-0'
|
||||
'grid transition-[grid-template-rows,opacity] duration-200 ease-in-out',
|
||||
open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
|
||||
)}
|
||||
>
|
||||
{/* 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 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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,36 @@
|
||||
'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 60,000 builders.</div>
|
||||
<div className='text-muted-foreground'>Trusted by over 100,000 builders.</div>
|
||||
<div className='text-muted-foreground'>
|
||||
Build Agentic workflows visually on a drag-and-drop canvas or with natural language.
|
||||
The open-source platform to build AI agents and run your agentic workforce.
|
||||
</div>
|
||||
<Link
|
||||
href='https://sim.ai/signup'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
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-[#2AAD6C] bg-gradient-to-b from-[#3ED990] to-[#2AAD6C] px-3 pr-[10px] pl-[12px] font-medium text-sm text-white shadow-[inset_0_2px_4px_0_#5EE8A8] outline-none transition-all hover:shadow-lg focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50'
|
||||
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'
|
||||
aria-label='Get started with Sim - Sign up for free'
|
||||
>
|
||||
<span>Get started</span>
|
||||
<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 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>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -526,6 +526,17 @@ export function SlackMonoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GammaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='-14 0 192 192' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='M47.2,14.4c-14.4,8.2-26,19.6-34.4,33.6C4.3,62.1,0,77.7,0,94.3s4.3,32.2,12.7,46.3c8.5,14.1,20,25.4,34.4,33.6,14.4,8.2,30.4,12.4,47.7,12.4h69.8v-112.5h-81v39.1h38.2v31.8h-25.6c-9.1,0-17.6-2.3-25.2-6.9-7.6-4.6-13.8-10.8-18.3-18.4-4.5-7.7-6.7-16.2-6.7-25.3s2.3-17.7,6.7-25.3c4.5-7.7,10.6-13.9,18.3-18.4,7.6-4.6,16.1-6.9,25.2-6.9h68.5V2h-69.8c-17.3,0-33.3,4.2-47.7,12.4h0Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GithubIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} width='26' height='26' viewBox='0 0 26 26' xmlns='http://www.w3.org/2000/svg'>
|
||||
@@ -537,6 +548,34 @@ export function GithubIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GithubOutlineIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='M15 21C15 21 15 18.73 15 18C15 17.37 15.15 16.04 14.5 15.5C15.89 15.37 16.98 14.92 18 14C19.02 13.08 19.5 11.69 19.5 9.5C19.5 8 19.25 7 18.5 6C18.79 5.22 18.84 4 18.5 3C16.94 3 15.53 4.07 15 4.5C14.61 4.4 13.67 4 12 4C10.33 4 9.39 4.4 9 4.5C8.47 4.07 7.06 3 5.5 3C5.16 4 5.21 5.22 5.5 6C4.75 7 4.5 8 4.5 9.5C4.5 11.69 4.98 13.08 6 14C7.02 14.92 8.11 15.37 9.5 15.5C8.85 16.04 9 17.37 9 18C9 18.73 9 21 9 21'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M9 19C7.59 19 6.16 18.44 5.31 17.81C4.47 17.18 4.22 16.15 3 15.5'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GitLabIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||||
@@ -699,6 +738,155 @@ export function PerplexityIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function ObsidianIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const bl = `${id}-bl`
|
||||
const tr = `${id}-tr`
|
||||
const tl = `${id}-tl`
|
||||
const br = `${id}-br`
|
||||
const te = `${id}-te`
|
||||
const le = `${id}-le`
|
||||
const be = `${id}-be`
|
||||
const me = `${id}-me`
|
||||
const clip = `${id}-clip`
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 512 512' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<radialGradient
|
||||
id={bl}
|
||||
cx='0'
|
||||
cy='0'
|
||||
gradientTransform='matrix(-59 -225 150 -39 161.4 470)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
r='1'
|
||||
>
|
||||
<stop offset='0' stopColor='#fff' stopOpacity='.4' />
|
||||
<stop offset='1' stopOpacity='.1' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={tr}
|
||||
cx='0'
|
||||
cy='0'
|
||||
gradientTransform='matrix(50 -379 280 37 360 374.2)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
r='1'
|
||||
>
|
||||
<stop offset='0' stopColor='#fff' stopOpacity='.6' />
|
||||
<stop offset='1' stopColor='#fff' stopOpacity='.1' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={tl}
|
||||
cx='0'
|
||||
cy='0'
|
||||
gradientTransform='matrix(69 -319 218 47 175.4 307)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
r='1'
|
||||
>
|
||||
<stop offset='0' stopColor='#fff' stopOpacity='.8' />
|
||||
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={br}
|
||||
cx='0'
|
||||
cy='0'
|
||||
gradientTransform='matrix(-96 -163 187 -111 335.3 512.2)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
r='1'
|
||||
>
|
||||
<stop offset='0' stopColor='#fff' stopOpacity='.3' />
|
||||
<stop offset='1' stopOpacity='.3' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={te}
|
||||
cx='0'
|
||||
cy='0'
|
||||
gradientTransform='matrix(-36 166 -112 -24 310 128.2)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
r='1'
|
||||
>
|
||||
<stop offset='0' stopColor='#fff' stopOpacity='0' />
|
||||
<stop offset='1' stopColor='#fff' stopOpacity='.2' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={le}
|
||||
cx='0'
|
||||
cy='0'
|
||||
gradientTransform='matrix(88 89 -190 187 111 220.2)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
r='1'
|
||||
>
|
||||
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
|
||||
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={be}
|
||||
cx='0'
|
||||
cy='0'
|
||||
gradientTransform='matrix(9 130 -276 20 215 284)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
r='1'
|
||||
>
|
||||
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
|
||||
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={me}
|
||||
cx='0'
|
||||
cy='0'
|
||||
gradientTransform='matrix(-198 -104 327 -623 400 399.2)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
r='1'
|
||||
>
|
||||
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
|
||||
<stop offset='.5' stopColor='#fff' stopOpacity='.2' />
|
||||
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
|
||||
</radialGradient>
|
||||
<clipPath id={clip}>
|
||||
<path d='M.2.2h512v512H.2z' />
|
||||
</clipPath>
|
||||
<g clipPath={`url(#${clip})`}>
|
||||
<path
|
||||
d='M382.3 475.6c-3.1 23.4-26 41.6-48.7 35.3-32.4-8.9-69.9-22.8-103.6-25.4l-51.7-4a34 34 0 0 1-22-10.2l-89-91.7a34 34 0 0 1-6.7-37.7s55-121 57.1-127.3c2-6.3 9.6-61.2 14-90.6 1.2-7.9 5-15 11-20.3L248 8.9a34.1 34.1 0 0 1 49.6 4.3L386 125.6a37 37 0 0 1 7.6 22.4c0 21.3 1.8 65 13.6 93.2 11.5 27.3 32.5 57 43.5 71.5a17.3 17.3 0 0 1 1.3 19.2 1494 1494 0 0 1-44.8 70.6c-15 22.3-21.9 49.9-25 73.1z'
|
||||
fill='#6c31e3'
|
||||
/>
|
||||
<path
|
||||
d='M165.9 478.3c41.4-84 40.2-144.2 22.6-187-16.2-39.6-46.3-64.5-70-80-.6 2.3-1.3 4.4-2.2 6.5L60.6 342a34 34 0 0 0 6.6 37.7l89.1 91.7a34 34 0 0 0 9.6 7z'
|
||||
fill={`url(#${bl})`}
|
||||
/>
|
||||
<path
|
||||
d='M278.4 307.8c11.2 1.2 22.2 3.6 32.8 7.6 34 12.7 65 41.2 90.5 96.3 1.8-3.1 3.6-6.2 5.6-9.2a1536 1536 0 0 0 44.8-70.6 17 17 0 0 0-1.3-19.2c-11-14.6-32-44.2-43.5-71.5-11.8-28.2-13.5-72-13.6-93.2 0-8.1-2.6-16-7.6-22.4L297.6 13.2a34 34 0 0 0-1.5-1.7 96 96 0 0 1 2 54 198.3 198.3 0 0 1-17.6 41.3l-7.2 14.2a171 171 0 0 0-19.4 71c-1.2 29.4 4.8 66.4 24.5 115.8z'
|
||||
fill={`url(#${tr})`}
|
||||
/>
|
||||
<path
|
||||
d='M278.4 307.8c-19.7-49.4-25.8-86.4-24.5-115.9a171 171 0 0 1 19.4-71c2.3-4.8 4.8-9.5 7.2-14.1 7.1-13.9 14-27 17.6-41.4a96 96 0 0 0-2-54A34.1 34.1 0 0 0 248 9l-105.4 94.8a34.1 34.1 0 0 0-10.9 20.3l-12.8 85-.5 2.3c23.8 15.5 54 40.4 70.1 80a147 147 0 0 1 7.8 24.8c28-6.8 55.7-11 82.1-8.3z'
|
||||
fill={`url(#${tl})`}
|
||||
/>
|
||||
<path
|
||||
d='M333.6 511c22.7 6.2 45.6-12 48.7-35.4a187 187 0 0 1 19.4-63.9c-25.6-55-56.5-83.6-90.4-96.3-36-13.4-75.2-9-115 .7 8.9 40.4 3.6 93.3-30.4 162.2 4 1.8 8.1 3 12.5 3.3 0 0 24.4 2 53.6 4.1 29 2 72.4 17.1 101.6 25.2z'
|
||||
fill={`url(#${br})`}
|
||||
/>
|
||||
<g clipRule='evenodd' fillRule='evenodd'>
|
||||
<path
|
||||
d='M254.1 190c-1.3 29.2 2.4 62.8 22.1 112.1l-6.2-.5c-17.7-51.5-21.5-78-20.2-107.6a174.7 174.7 0 0 1 20.4-72c2.4-4.9 8-14.1 10.5-18.8 7.1-13.7 11.9-21 16-33.6 5.7-17.5 4.5-25.9 3.8-34.1 4.6 29.9-12.7 56-25.7 82.4a177.1 177.1 0 0 0-20.7 72z'
|
||||
fill={`url(#${te})`}
|
||||
/>
|
||||
<path
|
||||
d='M194.3 293.4c2.4 5.4 4.6 9.8 6 16.5L195 311c-2.1-7.8-3.8-13.4-6.8-20-17.8-42-46.3-63.6-69.7-79.5 28.2 15.2 57.2 39 75.7 81.9z'
|
||||
fill={`url(#${le})`}
|
||||
/>
|
||||
<path
|
||||
d='M200.6 315.1c9.8 46-1.2 104.2-33.6 160.9 27.1-56.2 40.2-110.1 29.3-160z'
|
||||
fill={`url(#${be})`}
|
||||
/>
|
||||
<path
|
||||
d='M312.5 311c53.1 19.9 73.6 63.6 88.9 100-19-38.1-45.2-80.3-90.8-96-34.8-11.8-64.1-10.4-114.3 1l-1.1-5c53.2-12.1 81-13.5 117.3 0z'
|
||||
fill={`url(#${me})`}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function NotionIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' width='1em' height='1em' {...props}>
|
||||
@@ -939,6 +1127,25 @@ export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DevinIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 500 500' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M59.29,209.39l48.87,28.21c1.75,1.01,3.71,1.51,5.67,1.51c1.95,0,3.92-0.52,5.67-1.51l48.87-28.21c0,0,0.14-0.11,0.2-0.16c0.74-0.45,1.44-0.99,2.07-1.6c0.09-0.09,0.18-0.2,0.27-0.29c0.54-0.58,1.03-1.21,1.44-1.89c0.06-0.11,0.16-0.2,0.2-0.32c0.43-0.74,0.74-1.53,0.99-2.37c0.05-0.18,0.09-0.36,0.14-0.54c0.2-0.86,0.36-1.74,0.36-2.66v-28.21c0-10.89,5.87-21.03,15.3-26.48c9.42-5.45,21.15-5.44,30.59,0l24.43,14.11c0.79,0.45,1.62,0.77,2.47,1.01c0.18,0.05,0.37,0.11,0.54,0.16c0.83,0.2,1.69,0.32,2.54,0.34c0.05,0,0.09,0,0.11,0c0.09,0,0.18-0.05,0.26-0.05c0.79,0,1.58-0.11,2.34-0.32c0.14-0.03,0.27-0.05,0.4-0.09c0.83-0.23,1.64-0.57,2.41-0.99c0.06-0.05,0.16-0.05,0.23-0.09l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81V64.52c0-4.05-2.16-7.78-5.67-9.81l-48.91-28.19c-3.51-2.03-7.81-2.03-11.32,0l-48.87,28.21c0,0-0.14,0.11-0.2,0.16c-0.74,0.45-1.44,0.99-2.07,1.6c-0.09,0.09-0.18,0.2-0.27,0.29c-0.54,0.58-1.03,1.21-1.44,1.89c-0.06,0.11-0.16,0.2-0.2,0.31c-0.43,0.74-0.74,1.53-0.99,2.37c-0.05,0.18-0.09,0.36-0.14,0.54c-0.2,0.86-0.36,1.74-0.36,2.66v28.21c0,10.89-5.87,21.03-15.3,26.5c-9.42,5.44-21.15,5.44-30.59,0l-24.42-14.1c-0.79-0.45-1.63-0.77-2.47-1.01c-0.18-0.05-0.36-0.11-0.54-0.16c-0.84-0.2-1.69-0.31-2.55-0.34c-0.14,0-0.25,0-0.38,0c-0.81,0-1.6,0.11-2.37,0.31c-0.14,0.02-0.25,0.05-0.38,0.09c-0.82,0.23-1.63,0.57-2.4,1c-0.06,0.05-0.16,0.05-0.23,0.09l-48.84,28.24c-3.51,2.03-5.67,5.76-5.67,9.81v56.42c0,4.05,2.16,7.78,5.67,9.81C59.29,209.41,59.29,209.39,59.29,209.39z'
|
||||
fill='#2A6DCE'
|
||||
/>
|
||||
<path
|
||||
d='M325.46,223.49c9.42-5.44,21.15-5.44,30.59,0l24.43,14.11c0.79,0.45,1.62,0.77,2.47,1.01c0.18,0.05,0.36,0.11,0.54,0.16c0.83,0.2,1.69,0.31,2.54,0.34c0.05,0,0.09,0,0.11,0c0.09,0,0.18-0.03,0.26-0.05c0.79,0,1.58-0.11,2.34-0.31c0.14-0.03,0.27-0.05,0.4-0.09c0.83-0.23,1.62-0.57,2.41-0.99c0.06-0.05,0.16-0.05,0.25-0.09l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81l-48.84-28.22c-3.51-2.03-7.81-2.03-11.32,0l-48.87,28.21c0,0-0.14,0.11-0.2,0.16c-0.74,0.45-1.44,0.99-2.07,1.6c-0.09,0.09-0.18,0.2-0.26,0.29c-0.54,0.58-1.03,1.21-1.44,1.89c-0.06,0.11-0.16,0.2-0.2,0.32c-0.43,0.74-0.74,1.53-0.99,2.37c-0.05,0.18-0.09,0.36-0.14,0.54c-0.2,0.86-0.36,1.74-0.36,2.66v28.21c0,10.89-5.87,21.03-15.3,26.5c-9.42,5.44-21.15,5.44-30.59,0l-24.43-14.11c-0.79-0.45-1.62-0.77-2.47-1.01c-0.18-0.05-0.36-0.11-0.54-0.16c-0.83-0.2-1.69-0.32-2.54-0.34c-0.14,0-0.25,0-0.38,0c-0.81,0-1.6,0.11-2.37,0.32c-0.14,0.03-0.25,0.05-0.38,0.09c-0.83,0.23-1.64,0.57-2.41,0.99c-0.06,0.05-0.16,0.05-0.23,0.09l-48.87,28.21c-3.51,2.03-5.67,5.76-5.67,9.81v56.43c0,4.05,2.16,7.78,5.67,9.81l48.87,28.21c0,0,0.16,0.05,0.23,0.09c0.77,0.43,1.58,0.77,2.41,0.99c0.14,0.05,0.27,0.05,0.4,0.09c0.77,0.18,1.55,0.29,2.34,0.32c0.09,0,0.18,0.05,0.27,0.05c0.05,0,0.09,0,0.11,0c0.86,0,1.69-0.14,2.54-0.34c0.18-0.05,0.36-0.09,0.54-0.16c0.86-0.25,1.69-0.57,2.47-1.01l24.43-14.11c9.42-5.44,21.15-5.44,30.59,0c9.42,5.44,15.3,15.59,15.3,26.48v28.21c0,0.92,0.14,1.8,0.36,2.66c0.05,0.18,0.09,0.36,0.14,0.54c0.25,0.83,0.56,1.62,0.99,2.37c0.06,0.11,0.14,0.2,0.2,0.31c0.4,0.68,0.9,1.31,1.44,1.89c0.09,0.09,0.18,0.2,0.26,0.29c0.61,0.6,1.31,1.12,2.07,1.6c0.06,0.05,0.11,0.11,0.2,0.16l48.87,28.21c1.75,1.01,3.72,1.51,5.67,1.51s3.92-0.52,5.67-1.51l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81l-48.87-28.21c0,0-0.16-0.05-0.23-0.09c-0.77-0.43-1.58-0.77-2.41-0.99c-0.14-0.05-0.25-0.05-0.38-0.09c-0.79-0.18-1.57-0.29-2.38-0.32c-0.11,0-0.25,0-0.36,0c-0.86,0-1.71,0.14-2.54,0.34c-0.18,0.05-0.34,0.09-0.52,0.16c-0.86,0.25-1.69,0.57-2.47,1.01l-24.43,14.11c-9.42,5.44-21.15,5.44-30.58,0c-9.42-5.44-15.3-15.59-15.3-26.5c0-10.91,5.87-21.03,15.3-26.48C325.55,223.49,325.46,223.49,325.46,223.49z'
|
||||
fill='#1DC19C'
|
||||
/>
|
||||
<path
|
||||
d='M304.5,369.22l-48.87-28.21c0,0-0.16-0.05-0.23-0.09c-0.77-0.43-1.57-0.77-2.41-0.99c-0.14-0.05-0.27-0.05-0.4-0.09c-0.79-0.18-1.57-0.29-2.37-0.32c-0.14,0-0.25,0-0.38,0c-0.86,0-1.71,0.14-2.54,0.34c-0.18,0.05-0.34,0.09-0.52,0.16c-0.86,0.25-1.69,0.57-2.47,1.01l-24.43,14.11c-9.42,5.44-21.15,5.44-30.58,0c-9.42-5.44-15.3-15.59-15.3-26.5v-28.22c0-0.92-0.14-1.8-0.36-2.66c-0.05-0.18-0.09-0.36-0.14-0.54c-0.25-0.83-0.57-1.62-0.99-2.37c-0.06-0.11-0.14-0.2-0.2-0.32c-0.4-0.68-0.9-1.31-1.44-1.89c-0.09-0.09-0.18-0.2-0.27-0.29c-0.6-0.6-1.31-1.12-2.07-1.6c-0.06-0.05-0.11-0.11-0.2-0.16l-48.87-28.21c-3.51-2.03-7.81-2.03-11.32,0L59.28,290.6c-3.51,2.03-5.67,5.76-5.67,9.81v56.43c0,4.05,2.16,7.78,5.67,9.81l48.87,28.21c0,0,0.16,0.06,0.23,0.09c0.77,0.43,1.55,0.77,2.38,0.99c0.14,0.05,0.27,0.06,0.4,0.09c0.77,0.18,1.55,0.29,2.34,0.32c0.09,0,0.18,0.05,0.29,0.05c0.05,0,0.09,0,0.14,0c0.86,0,1.69-0.14,2.52-0.34c0.18-0.05,0.36-0.09,0.54-0.16c0.86-0.25,1.69-0.57,2.47-1.01l24.43-14.11c9.42-5.44,21.15-5.44,30.59,0c9.42,5.44,15.3,15.59,15.3,26.48v28.21c0,0.92,0.14,1.8,0.36,2.66c0.05,0.18,0.09,0.36,0.14,0.54c0.25,0.83,0.57,1.62,0.99,2.37c0.06,0.11,0.14,0.2,0.2,0.32c0.4,0.68,0.9,1.31,1.44,1.89c0.09,0.09,0.18,0.2,0.27,0.29c0.61,0.61,1.31,1.12,2.07,1.6c0.06,0.05,0.11,0.11,0.2,0.16l48.87,28.21c1.75,1.01,3.71,1.51,5.67,1.51c1.96,0,3.92-0.52,5.67-1.51l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81L304.5,369.22z'
|
||||
fill='#1796E2'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1179,6 +1386,17 @@ export function AlgoliaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function AmplitudeIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 49 49'>
|
||||
<path
|
||||
fill='#FFFFFF'
|
||||
d='M23.4,15.3c0.6,1.8,1.2,4.1,1.9,6.7c-2.6,0-5.3-0.1-7.8-0.1h-1.3c1.5-5.7,3.2-10.1,4.6-11.1 c0.1-0.1,0.2-0.1,0.4-0.1c0.2,0,0.3,0.1,0.5,0.3C21.9,11.5,22.5,12.7,23.4,15.3z M49,24.5C49,38,38,49,24.5,49S0,38,0,24.5 S11,0,24.5,0S49,11,49,24.5z M42.7,23.9c0-0.6-0.4-1.2-1-1.3l0,0l0,0l0,0c-0.1,0-0.1,0-0.2,0h-0.2c-4.1-0.3-8.4-0.4-12.4-0.5l0,0 C27,14.8,24.5,7.4,21.3,7.4c-3,0-5.8,4.9-8.2,14.5c-1.7,0-3.2,0-4.6-0.1c-0.1,0-0.2,0-0.2,0c-0.3,0-0.5,0-0.5,0 c-0.8,0.1-1.4,0.9-1.4,1.7c0,0.8,0.6,1.6,1.5,1.7l0,0h4.6c-0.4,1.9-0.8,3.8-1.1,5.6l-0.1,0.8l0,0c0,0.6,0.5,1.1,1.1,1.1 c0.4,0,0.8-0.2,1-0.5l0,0l2.2-7.1h10.7c0.8,3.1,1.7,6.3,2.8,9.3c0.6,1.6,2,5.4,4.4,5.4l0,0c3.6,0,5-5.8,5.9-9.6 c0.2-0.8,0.4-1.5,0.5-2.1l0.1-0.2l0,0c0-0.1,0-0.2,0-0.3c-0.1-0.2-0.2-0.3-0.4-0.4c-0.3-0.1-0.5,0.1-0.6,0.4l0,0l-0.1,0.2 c-0.3,0.8-0.6,1.6-0.8,2.3v0.1c-1.6,4.4-2.3,6.4-3.7,6.4l0,0l0,0l0,0c-1.8,0-3.5-7.3-4.1-10.1c-0.1-0.5-0.2-0.9-0.3-1.3h11.7 c0.2,0,0.4-0.1,0.6-0.1l0,0c0,0,0,0,0.1,0c0,0,0,0,0.1,0l0,0c0,0,0.1,0,0.1-0.1l0,0C42.5,24.6,42.7,24.3,42.7,23.9z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleBooksIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 478.633 540.068'>
|
||||
@@ -1235,6 +1453,20 @@ export function GoogleSlidesIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleContactsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 500'>
|
||||
<path fill='#86a9ff' d='M199 244c-89 0-161 71-161 160v67c0 16 13 29 29 29h77l77-256z' />
|
||||
<path fill='#578cff' d='M462 349c0-58-48-105-106-105h-77v256h77c58 0 106-47 106-106' />
|
||||
<path
|
||||
fill='#0057cc'
|
||||
d='M115 349c0-58 48-105 106-105h58c58 0 106 47 106 105v45c0 59-48 106-106 106H144c-16 0-29-13-29-29z'
|
||||
/>
|
||||
<circle cx='250' cy='99.4' r='99.4' fill='#0057cc' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1302,6 +1534,21 @@ export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleTasksIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 527.1 500' xmlns='http://www.w3.org/2000/svg'>
|
||||
<polygon
|
||||
fill='#0066DA'
|
||||
points='410.4,58.3 368.8,81.2 348.2,120.6 368.8,168.8 407.8,211 450,187.5 475.9,142.8 450,87.5'
|
||||
/>
|
||||
<path
|
||||
fill='#2684FC'
|
||||
d='M249.3,219.4l98.9-98.9c29.1,22.1,50.5,53.8,59.6,90.4L272.1,346.7c-12.2,12.2-32,12.2-44.2,0l-91.5-91.5 c-9.8-9.8-9.8-25.6,0-35.3l39-39c9.8-9.8,25.6-9.8,35.3,0L249.3,219.4z M519.8,63.6l-39.7-39.7c-9.7-9.7-25.6-9.7-35.3,0 l-34.4,34.4c27.5,23,49.9,51.8,65.5,84.5l43.9-43.9C529.6,89.2,529.6,73.3,519.8,63.6z M412.5,250c0,89.8-72.8,162.5-162.5,162.5 S87.5,339.8,87.5,250S160.2,87.5,250,87.5c36.9,0,70.9,12.3,98.2,33.1l62.2-62.2C367,21.9,311.1,0,250,0C111.9,0,0,111.9,0,250 s111.9,250,250,250s250-111.9,250-250c0-38.3-8.7-74.7-24.1-107.2L407.8,211C410.8,223.5,412.5,236.6,412.5,250z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SupabaseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const gradient0 = `supabase_paint0_${id}`
|
||||
@@ -1641,167 +1888,42 @@ export function StagehandIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width='108'
|
||||
height='159'
|
||||
viewBox='0 0 108 159'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='256'
|
||||
height='352'
|
||||
viewBox='0 0 256 352'
|
||||
fill='none'
|
||||
>
|
||||
<path
|
||||
d='M15 26C22.8234 31.822 23.619 41.405 25.3125 50.3867C25.8461 53.1914 26.4211 55.9689 27.0625 58.75C27.7987 61.9868 28.4177 65.2319 29 68.5C29.332 70.3336 29.6653 72.1669 30 74C30.1418 74.7863 30.2836 75.5727 30.4297 76.3828C31.8011 83.2882 33.3851 90.5397 39.4375 94.75C40.3405 95.3069 40.3405 95.3069 41.2617 95.875C43.8517 97.5512 45.826 99.826 48 102C50.6705 102.89 52.3407 103.143 55.0898 103.211C55.8742 103.239 56.6586 103.268 57.4668 103.297C59.1098 103.349 60.7531 103.393 62.3965 103.43C65.8896 103.567 68.4123 103.705 71.5664 105.289C73 107 73 107 73 111C73.66 111 74.32 111 75 111C74.0759 106.912 74.0759 106.912 71.4766 103.828C67.0509 102.348 62.3634 102.64 57.7305 102.609C52.3632 102.449 49.2783 101.537 45 98C41.8212 94.0795 41.5303 90.9791 42 86C44.9846 83.0154 48.2994 83.6556 52.3047 83.6289C53.139 83.6199 53.9734 83.6108 54.833 83.6015C56.6067 83.587 58.3805 83.5782 60.1543 83.5745C62.8304 83.5627 65.5041 83.5137 68.1797 83.4629C81.1788 83.34 91.8042 85.3227 102 94C106.37 100.042 105.483 106.273 104.754 113.406C103.821 119.026 101.968 124.375 100.125 129.75C99.8806 130.471 99.6361 131.193 99.3843 131.936C97.7783 136.447 95.9466 140.206 93 144C92.34 144 91.68 144 91 144C91 144.66 91 145.32 91 146C79.0816 156.115 63.9798 156.979 49 156C36.6394 154.226 26.7567 148.879 19 139C11.0548 125.712 11.6846 105.465 11.3782 90.4719C11.0579 77.4745 8.03411 64.8142 5.4536 52.1135C5.04373 50.0912 4.64233 48.0673 4.24218 46.043C4.00354 44.8573 3.7649 43.6716 3.51903 42.45C2.14425 33.3121 2.14425 33.3121 4.87499 29.125C8.18297 25.817 10.3605 25.4542 15 26Z'
|
||||
fill='#FDFDFD'
|
||||
d='M 242.29,45.79 C 242.29,28.88 226.69,13.76 206.61,13.76 C 188.59,13.76 174.82,28.66 174.82,45.85 V 101.97 C 168.89,98.09 163.18,96.76 157.14,96.76 C 145.94,96.76 137.02,101.49 128.83,110.17 C 121.81,101.01 112.07,95.73 100.72,95.73 C 93.97,95.73 87.82,98.09 82.11,100.9 V 80.05 C 82.11,64.08 66.14,47.28 48.74,47.28 C 31.12,47.28 14.54,62.71 14.54,78.79 V 219.4 C 14.54,273.71 56.99,337.89 125.23,337.89 C 197.41,337.89 242.29,289.05 242.29,186.01 V 78.9 L 242.29,45.79 Z'
|
||||
fill='black'
|
||||
/>
|
||||
<path
|
||||
d='M91 0.999996C94.8466 2.96604 96.2332 5.08365 97.6091 9.03564C99.203 14.0664 99.4412 18.7459 99.4414 23.9922C99.4538 24.9285 99.4663 25.8647 99.4791 26.8294C99.5049 28.8198 99.5247 30.8103 99.539 32.8008C99.5785 37.9693 99.6682 43.1369 99.7578 48.3047C99.7747 49.3188 99.7917 50.3328 99.8091 51.3776C99.9603 59.6066 100.323 67.7921 100.937 76C101.012 77.0582 101.087 78.1163 101.164 79.2065C101.646 85.1097 102.203 90.3442 105.602 95.3672C107.492 98.9262 107.45 102.194 107.375 106.125C107.366 106.881 107.356 107.638 107.346 108.417C107.18 114.639 106.185 120.152 104 126C103.636 126.996 103.273 127.993 102.898 129.02C98.2182 141.022 92.6784 149.349 80.7891 155.062C67.479 160.366 49.4234 159.559 36 155C32.4272 153.286 29.2162 151.308 26 149C25.0719 148.361 24.1437 147.721 23.1875 147.062C8.32968 133.054 9.60387 109.231 8.73413 90.3208C8.32766 81.776 7.51814 73.4295 5.99999 65C5.82831 64.0338 5.65662 63.0675 5.47973 62.072C4.98196 59.3363 4.46395 56.6053 3.93749 53.875C3.76412 52.9572 3.59074 52.0394 3.4121 51.0938C2.75101 47.6388 2.11387 44.3416 0.999995 41C0.505898 36.899 0.0476353 32.7768 2.04687 29.0469C4.91881 25.5668 6.78357 24.117 11.25 23.6875C15.8364 24.0697 17.5999 24.9021 21 28C24.7763 34.3881 26.047 41.2626 27.1875 48.5C27.5111 50.4693 27.8377 52.4381 28.168 54.4062C28.3733 55.695 28.3733 55.695 28.5828 57.0098C28.8087 58.991 28.8087 58.991 30 60C30.3171 59.4947 30.6342 58.9894 30.9609 58.4688C33.1122 55.4736 34.7097 53.3284 38.3789 52.3945C44.352 52.203 48.1389 53.6183 53 57C53.0928 56.1338 53.0928 56.1338 53.1875 55.25C54.4089 51.8676 55.9015 50.8075 59 49C63.8651 48.104 66.9348 48.3122 71.1487 51.0332C72.0896 51.6822 73.0305 52.3313 74 53C73.9686 51.2986 73.9686 51.2986 73.9365 49.5627C73.8636 45.3192 73.818 41.0758 73.7803 36.8318C73.7603 35.0016 73.733 33.1715 73.6982 31.3415C73.6492 28.6976 73.6269 26.0545 73.6094 23.4102C73.5887 22.6035 73.5681 21.7969 73.5468 20.9658C73.5441 13.8444 75.5121 7.83341 80.25 2.4375C83.9645 0.495841 86.8954 0.209055 91 0.999996ZM3.99999 30C1.56925 34.8615 3.215 40.9393 4.24218 46.043C4.37061 46.6927 4.49905 47.3424 4.63137 48.0118C5.03968 50.0717 5.45687 52.1296 5.87499 54.1875C11.1768 80.6177 11.1768 80.6177 11.4375 93.375C11.7542 120.78 11.7542 120.78 23.5625 144.375C28.5565 149.002 33.5798 151.815 40 154C40.6922 154.244 41.3844 154.487 42.0977 154.738C55.6463 158.576 72.4909 156.79 84.8086 150.316C87.0103 148.994 89.0458 147.669 91 146C91 145.34 91 144.68 91 144C91.66 144 92.32 144 93 144C97.1202 138.98 99.3206 133.053 101.25 126.937C101.505 126.174 101.76 125.41 102.023 124.623C104.94 115.65 107.293 104.629 103.625 95.625C96.3369 88.3369 86.5231 83.6919 76.1988 83.6088C74.9905 83.6226 74.9905 83.6226 73.7578 83.6367C72.9082 83.6362 72.0586 83.6357 71.1833 83.6352C69.4034 83.6375 67.6235 83.6472 65.8437 83.6638C63.1117 83.6876 60.3806 83.6843 57.6484 83.6777C55.9141 83.6833 54.1797 83.6904 52.4453 83.6992C51.6277 83.6983 50.81 83.6974 49.9676 83.6964C45.5122 83.571 45.5122 83.571 42 86C41.517 90.1855 41.733 92.4858 43.6875 96.25C46.4096 99.4871 48.6807 101.674 53.0105 102.282C55.3425 102.411 57.6645 102.473 60 102.5C69.8847 102.612 69.8847 102.612 74 106C74.8125 108.687 74.8125 108.688 75 111C74.34 111 73.68 111 73 111C72.8969 110.216 72.7937 109.432 72.6875 108.625C72.224 105.67 72.224 105.67 69 104C65.2788 103.745 61.5953 103.634 57.8672 103.609C51.1596 103.409 46.859 101.691 41.875 97C41.2562 96.34 40.6375 95.68 40 95C39.175 94.4637 38.35 93.9275 37.5 93.375C30.9449 87.1477 30.3616 77.9789 29.4922 69.418C29.1557 66.1103 29.1557 66.1103 28.0352 63.625C26.5234 59.7915 26.1286 55.8785 25.5625 51.8125C23.9233 38.3 23.9233 38.3 17 27C11.7018 24.3509 7.9915 26.1225 3.99999 30Z'
|
||||
fill='#1F1F1F'
|
||||
d='M 224.94,46.23 C 224.94,36.76 215.91,28.66 205.91,28.66 C 196.75,28.66 189.9,36.11 189.9,45.14 V 152.72 C 202.88,153.38 214.08,155.96 224.94,166.19 V 78.79 L 224.94,46.23 Z'
|
||||
fill='white'
|
||||
/>
|
||||
<path
|
||||
d='M89.0976 2.53906C91 3 91 3 93.4375 5.3125C96.1586 9.99276 96.178 14.1126 96.2461 19.3828C96.2778 21.1137 96.3098 22.8446 96.342 24.5754C96.3574 25.4822 96.3728 26.3889 96.3887 27.3232C96.6322 41.3036 96.9728 55.2117 98.3396 69.1353C98.9824 75.7746 99.0977 82.3308 99 89C96.5041 88.0049 94.0126 87.0053 91.5351 85.9648C90.3112 85.4563 90.3112 85.4563 89.0625 84.9375C87.8424 84.4251 87.8424 84.4251 86.5976 83.9023C83.7463 82.9119 80.9774 82.4654 78 82C76.7702 65.9379 75.7895 49.8907 75.7004 33.7775C75.6919 32.3138 75.6783 30.8501 75.6594 29.3865C75.5553 20.4082 75.6056 12.1544 80.6875 4.4375C83.6031 2.62508 85.7 2.37456 89.0976 2.53906Z'
|
||||
fill='#FBFBFB'
|
||||
d='M 157.21,113.21 C 146.12,113.21 137.93,122.02 137.93,131.76 V 154.62 C 142.24,153.05 145.95,152.61 149.83,152.61 H 174.71 V 131.76 C 174.71,122.35 166.73,113.21 157.21,113.21 Z'
|
||||
fill='white'
|
||||
/>
|
||||
<path
|
||||
d='M97 13C97.99 13.495 97.99 13.495 99 14C99.0297 15.8781 99.0297 15.8781 99.0601 17.7942C99.4473 46.9184 99.4473 46.9184 100.937 76C101.012 77.0574 101.087 78.1149 101.164 79.2043C101.646 85.1082 102.203 90.3434 105.602 95.3672C107.492 98.9262 107.45 102.194 107.375 106.125C107.366 106.881 107.356 107.638 107.346 108.417C107.18 114.639 106.185 120.152 104 126C103.636 126.996 103.273 127.993 102.898 129.02C98.2182 141.022 92.6784 149.349 80.7891 155.062C67.479 160.366 49.4234 159.559 36 155C32.4272 153.286 29.2162 151.308 26 149C24.6078 148.041 24.6078 148.041 23.1875 147.062C13.5484 137.974 10.832 124.805 9.99999 112C9.91815 101.992 10.4358 91.9898 11 82C11.33 82 11.66 82 12 82C12.0146 82.6118 12.0292 83.2236 12.0442 83.854C11.5946 115.845 11.5946 115.845 24.0625 143.875C28.854 148.273 33.89 150.868 40 153C40.6935 153.245 41.387 153.49 42.1016 153.742C56.9033 157.914 73.8284 155.325 87 148C88.3301 147.327 89.6624 146.658 91 146C91 145.34 91 144.68 91 144C91.66 144 92.32 144 93 144C100.044 130.286 105.786 114.602 104 99C102.157 94.9722 100.121 93.0631 96.3125 90.875C95.5042 90.398 94.696 89.9211 93.8633 89.4297C85.199 85.1035 78.1558 84.4842 68.5 84.3125C67.2006 84.2783 65.9012 84.2442 64.5625 84.209C61.3751 84.127 58.1879 84.0577 55 84C55 83.67 55 83.34 55 83C58.9087 82.7294 62.8179 82.4974 66.7309 82.2981C68.7007 82.1902 70.6688 82.0535 72.6367 81.916C82.854 81.4233 90.4653 83.3102 99 89C98.8637 87.6094 98.8637 87.6094 98.7246 86.1907C96.96 67.8915 95.697 49.7051 95.75 31.3125C95.751 30.5016 95.7521 29.6908 95.7532 28.8554C95.7901 15.4198 95.7901 15.4198 97 13Z'
|
||||
fill='#262114'
|
||||
d='M 100.06,111.75 C 89.19,111.75 81.85,121.06 81.85,130.31 V 157.86 C 81.85,167.71 89.72,175.38 99.24,175.38 C 109.71,175.38 118.39,166.91 118.39,157.39 V 130.31 C 118.39,120.79 110.03,111.75 100.06,111.75 Z'
|
||||
fill='white'
|
||||
/>
|
||||
<path
|
||||
d='M68 51C72.86 54.06 74.644 56.5072 76 62C76.249 65.2763 76.2347 68.5285 76.1875 71.8125C76.1868 72.6833 76.1862 73.554 76.1855 74.4512C76.1406 80.8594 76.1406 80.8594 75 82C73.5113 82.0867 72.0185 82.107 70.5273 82.0976C69.6282 82.0944 68.7291 82.0912 67.8027 82.0879C66.8572 82.0795 65.9117 82.0711 64.9375 82.0625C63.9881 82.058 63.0387 82.0535 62.0605 82.0488C59.707 82.037 57.3535 82.0205 55 82C53.6352 77.2188 53.738 72.5029 53.6875 67.5625C53.6585 66.6208 53.6295 65.6792 53.5996 64.709C53.5591 60.2932 53.5488 57.7378 55.8945 53.9023C59.5767 50.5754 63.1766 50.211 68 51Z'
|
||||
fill='#F8F8F8'
|
||||
d='M 192.04,168.87 H 150.16 C 140.19,168.87 133.34,175.39 133.34,183.86 C 133.34,192.9 140.19,199.75 148.66,199.75 H 182.52 C 188.01,199.75 189.63,204.81 189.63,207.49 C 189.63,211.91 186.37,214.64 181.09,215.51 C 162.96,218.66 137.71,229.13 137.71,259.68 C 137.71,265.07 133.67,267.42 130.29,267.42 C 126.09,267.42 122.38,264.74 122.38,260.12 C 122.38,241.15 129.02,228.17 143.26,214.81 C 131.01,212.02 119.21,202.99 117.75,186.43 C 111.93,189.81 107.2,191.15 100.18,191.15 C 82.11,191.15 66.68,176.58 66.68,158.29 V 80.71 C 66.68,71.24 57.16,63.5 49.18,63.5 C 38.71,63.5 29.89,72.42 29.89,80.27 V 217.19 C 29.89,266.48 68.71,322.19 124.88,322.19 C 185.91,322.19 223.91,282.15 223.91,207.16 C 223.91,187.19 214.28,168.87 192.04,168.87 Z'
|
||||
fill='white'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function BrandfetchIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 29 31' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M46 55C48.7557 57.1816 50.4359 58.8718 52 62C52.0837 63.5215 52.1073 65.0466 52.0977 66.5703C52.0944 67.4662 52.0912 68.3621 52.0879 69.2852C52.0795 70.2223 52.0711 71.1595 52.0625 72.125C52.058 73.0699 52.0535 74.0148 52.0488 74.9883C52.037 77.3256 52.0206 79.6628 52 82C50.9346 82.1992 50.9346 82.1992 49.8477 82.4023C48.9286 82.5789 48.0094 82.7555 47.0625 82.9375C46.146 83.1115 45.2294 83.2855 44.2852 83.4648C42.0471 83.7771 42.0471 83.7771 41 85C40.7692 86.3475 40.5885 87.7038 40.4375 89.0625C40.2931 90.3619 40.1487 91.6613 40 93C37 92 37 92 35.8672 90.1094C35.5398 89.3308 35.2123 88.5522 34.875 87.75C34.5424 86.9817 34.2098 86.2134 33.8672 85.4219C31.9715 80.1277 31.7884 75.065 31.75 69.5C31.7294 68.7536 31.7087 68.0073 31.6875 67.2383C31.6551 62.6607 32.0474 59.7266 35 56C38.4726 54.2637 42.2119 54.3981 46 55Z'
|
||||
fill='#FAFAFA'
|
||||
/>
|
||||
<path
|
||||
d='M97 13C97.66 13.33 98.32 13.66 99 14C99.0297 15.8781 99.0297 15.8781 99.0601 17.7942C99.4473 46.9184 99.4473 46.9184 100.937 76C101.012 77.0574 101.087 78.1149 101.164 79.2043C101.566 84.1265 102.275 88.3364 104 93C103.625 95.375 103.625 95.375 103 97C102.361 96.2781 101.721 95.5563 101.062 94.8125C94.4402 88.1902 85.5236 84.8401 76.2891 84.5859C75.0451 84.5473 73.8012 84.5086 72.5195 84.4688C71.2343 84.4378 69.9491 84.4069 68.625 84.375C66.6624 84.317 66.6624 84.317 64.6601 84.2578C61.4402 84.1638 58.2203 84.0781 55 84C55 83.67 55 83.34 55 83C58.9087 82.7294 62.8179 82.4974 66.7309 82.2981C68.7007 82.1902 70.6688 82.0535 72.6367 81.916C82.854 81.4233 90.4653 83.3102 99 89C98.9091 88.0729 98.8182 87.1458 98.7246 86.1907C96.96 67.8915 95.697 49.7051 95.75 31.3125C95.751 30.5016 95.7521 29.6908 95.7532 28.8554C95.7901 15.4198 95.7901 15.4198 97 13Z'
|
||||
fill='#423B28'
|
||||
/>
|
||||
<path
|
||||
d='M91 0.999996C94.3999 3.06951 96.8587 5.11957 98 9C97.625 12.25 97.625 12.25 97 15C95.804 12.6081 94.6146 10.2139 93.4375 7.8125C92.265 5.16236 92.265 5.16236 91 4C88.074 3.7122 85.8483 3.51695 83 4C79.1128 7.37574 78.178 11.0991 77 16C76.8329 18.5621 76.7615 21.1317 76.7695 23.6992C76.77 24.4155 76.7704 25.1318 76.7709 25.8698C76.7739 27.3783 76.7817 28.8868 76.7942 30.3953C76.8123 32.664 76.8147 34.9324 76.8144 37.2012C76.8329 44.6001 77.0765 51.888 77.7795 59.259C78.1413 63.7564 78.1068 68.2413 78.0625 72.75C78.058 73.6498 78.0535 74.5495 78.0488 75.4766C78.0373 77.6511 78.0193 79.8255 78 82C78.99 82.495 78.99 82.495 80 83C68.78 83.33 57.56 83.66 46 84C46.495 83.01 46.495 83.01 47 82C52.9349 80.7196 58.8909 80.8838 64.9375 80.9375C65.9075 80.942 66.8775 80.9465 67.8769 80.9512C70.2514 80.9629 72.6256 80.9793 75 81C75.0544 77.9997 75.0939 75.0005 75.125 72C75.1418 71.1608 75.1585 70.3216 75.1758 69.457C75.2185 63.9475 74.555 59.2895 73 54C73.66 54 74.32 54 75 54C74.9314 53.2211 74.8629 52.4422 74.7922 51.6396C74.1158 43.5036 73.7568 35.4131 73.6875 27.25C73.644 25.5194 73.644 25.5194 73.5996 23.7539C73.5376 15.3866 74.6189 8.85069 80.25 2.4375C83.9433 0.506911 86.9162 0.173322 91 0.999996Z'
|
||||
fill='#131311'
|
||||
/>
|
||||
<path
|
||||
d='M15 24C20.2332 26.3601 22.1726 29.3732 24.1875 34.5195C26.8667 42.6988 27.2651 50.4282 27 59C26.67 59 26.34 59 26 59C25.8945 58.436 25.7891 57.8721 25.6804 57.291C25.1901 54.6926 24.6889 52.0963 24.1875 49.5C24.0218 48.6131 23.8562 47.7262 23.6855 46.8125C21.7568 35.5689 21.7568 35.5689 15 27C12.0431 26.2498 12.0431 26.2498 8.99999 27C5.97965 28.9369 5.97965 28.9369 3.99999 32C3.67226 36.9682 4.31774 41.4911 5.27733 46.3594C5.40814 47.0304 5.53894 47.7015 5.67371 48.3929C5.94892 49.7985 6.22723 51.2035 6.50854 52.6079C6.93887 54.7569 7.35989 56.9075 7.77929 59.0586C9.09359 66.104 9.09359 66.104 11 73C11.0836 75.2109 11.1073 77.4243 11.0976 79.6367C11.0944 80.9354 11.0912 82.2342 11.0879 83.5723C11.0795 84.944 11.0711 86.3158 11.0625 87.6875C11.0575 89.071 11.0529 90.4544 11.0488 91.8379C11.037 95.2253 11.0206 98.6126 11 102C8.54975 99.5498 8.73228 98.8194 8.65624 95.4492C8.62812 94.53 8.60001 93.6108 8.57104 92.6638C8.54759 91.6816 8.52415 90.6994 8.49999 89.6875C8.20265 81.3063 7.58164 73.2485 5.99999 65C5.67135 63.2175 5.34327 61.435 5.01562 59.6523C4.31985 55.9098 3.62013 52.1681 2.90233 48.4297C2.75272 47.6484 2.60311 46.867 2.44897 46.062C1.99909 43.8187 1.99909 43.8187 0.999995 41C0.505898 36.899 0.0476353 32.7768 2.04687 29.0469C6.06003 24.1839 8.81126 23.4843 15 24Z'
|
||||
fill='#2A2311'
|
||||
/>
|
||||
<path
|
||||
d='M11 82C11.33 82 11.66 82 12 82C12.0146 82.6118 12.0292 83.2236 12.0442 83.854C11.5946 115.845 11.5946 115.845 24.0625 143.875C30.0569 149.404 36.9894 152.617 45 154C42 156 42 156 39.4375 156C29.964 153.244 20.8381 146.677 16 138C8.26993 120.062 9.92611 101.014 11 82Z'
|
||||
fill='#272214'
|
||||
/>
|
||||
<path
|
||||
d='M68 49C70.3478 50.1116 71.9703 51.3346 74 53C73.34 53.66 72.68 54.32 72 55C71.505 54.505 71.01 54.01 70.5 53.5C67.6718 51.8031 65.3662 51.5622 62.0976 51.4062C58.4026 52.4521 57.1992 53.8264 55 57C54.3826 61.2861 54.5302 65.4938 54.6875 69.8125C54.7101 70.9823 54.7326 72.1521 54.7559 73.3574C54.8147 76.2396 54.8968 79.1191 55 82C54.01 82 53.02 82 52 82C51.9854 81.4203 51.9708 80.8407 51.9558 80.2434C51.881 77.5991 51.7845 74.9561 51.6875 72.3125C51.6649 71.4005 51.6424 70.4885 51.6191 69.5488C51.4223 64.6292 51.2621 60.9548 48 57C45.6603 55.8302 44.1661 55.8339 41.5625 55.8125C40.78 55.7983 39.9976 55.7841 39.1914 55.7695C36.7079 55.8591 36.7079 55.8591 34 58C32.7955 60.5518 32.7955 60.5518 32 63C31.34 63 30.68 63 30 63C30.2839 59.6879 31.0332 57.9518 32.9375 55.1875C36.7018 52.4987 38.9555 52.3484 43.4844 52.5586C47.3251 53.2325 49.8148 54.7842 53 57C53.0928 56.1338 53.0928 56.1338 53.1875 55.25C55.6091 48.544 61.7788 47.8649 68 49Z'
|
||||
fill='#1F1A0F'
|
||||
/>
|
||||
<path
|
||||
d='M99 60C99.33 60 99.66 60 100 60C100.05 60.7865 100.1 61.573 100.152 62.3833C100.385 65.9645 100.63 69.5447 100.875 73.125C100.954 74.3625 101.032 75.6 101.113 76.875C101.197 78.0738 101.281 79.2727 101.367 80.5078C101.44 81.6075 101.514 82.7073 101.589 83.8403C102.013 87.1 102.94 89.8988 104 93C103.625 95.375 103.625 95.375 103 97C102.361 96.2781 101.721 95.5563 101.062 94.8125C94.4402 88.1902 85.5236 84.8401 76.2891 84.5859C74.4231 84.5279 74.4231 84.5279 72.5195 84.4688C71.2343 84.4378 69.9491 84.4069 68.625 84.375C67.3166 84.3363 66.0082 84.2977 64.6601 84.2578C61.4402 84.1638 58.2203 84.0781 55 84C55 83.67 55 83.34 55 83C58.9087 82.7294 62.8179 82.4974 66.7309 82.2981C68.7007 82.1902 70.6688 82.0535 72.6367 81.916C82.854 81.4233 90.4653 83.3102 99 89C98.9162 87.912 98.8324 86.8241 98.7461 85.7031C98.1266 77.012 97.9127 68.6814 99 60Z'
|
||||
fill='#332E22'
|
||||
/>
|
||||
<path
|
||||
d='M15 24C20.2332 26.3601 22.1726 29.3732 24.1875 34.5195C26.8667 42.6988 27.2651 50.4282 27 59C26.67 59 26.34 59 26 59C25.8945 58.436 25.7891 57.8721 25.6804 57.291C25.1901 54.6926 24.6889 52.0963 24.1875 49.5C24.0218 48.6131 23.8562 47.7262 23.6855 46.8125C21.7568 35.5689 21.7568 35.5689 15 27C12.0431 26.2498 12.0431 26.2498 8.99999 27C5.2818 29.7267 4.15499 31.2727 3.18749 35.8125C3.12562 36.8644 3.06374 37.9163 2.99999 39C2.33999 39 1.67999 39 0.999992 39C0.330349 31.2321 0.330349 31.2321 3.37499 27.5625C7.31431 23.717 9.51597 23.543 15 24Z'
|
||||
fill='#1D180A'
|
||||
/>
|
||||
<path
|
||||
d='M91 0.999996C94.3999 3.06951 96.8587 5.11957 98 9C97.625 12.25 97.625 12.25 97 15C95.804 12.6081 94.6146 10.2139 93.4375 7.8125C92.265 5.16236 92.265 5.16236 91 4C85.4345 3.33492 85.4345 3.33491 80.6875 5.75C78.5543 9.85841 77.6475 13.9354 76.7109 18.4531C76.4763 19.2936 76.2417 20.1341 76 21C75.34 21.33 74.68 21.66 74 22C73.5207 15.4102 74.5846 10.6998 78 5C81.755 0.723465 85.5463 -0.103998 91 0.999996Z'
|
||||
fill='#16130D'
|
||||
/>
|
||||
<path
|
||||
d='M42 93C42.5569 93.7631 43.1137 94.5263 43.6875 95.3125C46.4238 98.4926 48.7165 100.679 53.0105 101.282C55.3425 101.411 57.6646 101.473 60 101.5C70.6207 101.621 70.6207 101.621 75 106C75.0406 107.666 75.0427 109.334 75 111C74.34 111 73.68 111 73 111C72.7112 110.196 72.4225 109.391 72.125 108.562C71.2674 105.867 71.2674 105.867 69 105C65.3044 104.833 61.615 104.703 57.916 104.658C52.1631 104.454 48.7484 103.292 44 100C41.5625 97.25 41.5625 97.25 40 95C40.66 95 41.32 95 42 95C42 94.34 42 93.68 42 93Z'
|
||||
fill='#2B2B2B'
|
||||
/>
|
||||
<path
|
||||
d='M11 82C11.33 82 11.66 82 12 82C12.1682 86.6079 12.3287 91.216 12.4822 95.8245C12.5354 97.3909 12.5907 98.9574 12.6482 100.524C12.7306 102.78 12.8055 105.036 12.8789 107.293C12.9059 107.989 12.933 108.685 12.9608 109.402C13.0731 113.092 12.9015 116.415 12 120C11.67 120 11.34 120 11 120C9.63778 112.17 10.1119 104.4 10.4375 96.5C10.4908 95.0912 10.5436 93.6823 10.5957 92.2734C10.7247 88.8487 10.8596 85.4243 11 82Z'
|
||||
fill='#4D483B'
|
||||
/>
|
||||
<path
|
||||
d='M43.4844 52.5586C47.3251 53.2325 49.8148 54.7842 53 57C52 59 52 59 50 60C49.5256 59.34 49.0512 58.68 48.5625 58C45.2656 55.4268 43.184 55.5955 39.1211 55.6641C36.7043 55.8955 36.7043 55.8955 34 58C32.7955 60.5518 32.7955 60.5518 32 63C31.34 63 30.68 63 30 63C30.2839 59.6879 31.0332 57.9518 32.9375 55.1875C36.7018 52.4987 38.9555 52.3484 43.4844 52.5586Z'
|
||||
fill='#221F16'
|
||||
/>
|
||||
<path
|
||||
d='M76 73C76.33 73 76.66 73 77 73C77 75.97 77 78.94 77 82C78.485 82.495 78.485 82.495 80 83C68.78 83.33 57.56 83.66 46 84C46.33 83.34 46.66 82.68 47 82C52.9349 80.7196 58.8909 80.8838 64.9375 80.9375C65.9075 80.942 66.8775 80.9465 67.8769 80.9512C70.2514 80.9629 72.6256 80.9793 75 81C75.33 78.36 75.66 75.72 76 73Z'
|
||||
fill='#040404'
|
||||
/>
|
||||
<path
|
||||
d='M27 54C27.33 54 27.66 54 28 54C28.33 56.97 28.66 59.94 29 63C29.99 63 30.98 63 32 63C32 66.96 32 70.92 32 75C31.01 74.67 30.02 74.34 29 74C28.8672 73.2523 28.7344 72.5047 28.5977 71.7344C28.421 70.7495 28.2444 69.7647 28.0625 68.75C27.8885 67.7755 27.7144 66.8009 27.5352 65.7969C27.0533 63.087 27.0533 63.087 26.4062 60.8125C25.8547 58.3515 26.3956 56.4176 27 54Z'
|
||||
fill='#434039'
|
||||
/>
|
||||
<path
|
||||
d='M78 5C78.99 5.33 79.98 5.66 81 6C80.3194 6.92812 80.3194 6.92812 79.625 7.875C77.7233 11.532 77.1555 14.8461 76.5273 18.8906C76.3533 19.5867 76.1793 20.2828 76 21C75.34 21.33 74.68 21.66 74 22C73.5126 15.2987 74.9229 10.9344 78 5Z'
|
||||
fill='#2A2313'
|
||||
/>
|
||||
<path
|
||||
d='M12 115C12.99 115.495 12.99 115.495 14 116C14.5334 118.483 14.9326 120.864 15.25 123.375C15.3531 124.061 15.4562 124.747 15.5625 125.453C16.0763 129.337 16.2441 130.634 14 134C12.6761 127.57 11.752 121.571 12 115Z'
|
||||
fill='#2F2C22'
|
||||
/>
|
||||
<path
|
||||
d='M104 95C107 98 107 98 107.363 101.031C107.347 102.176 107.33 103.321 107.312 104.5C107.309 105.645 107.305 106.789 107.301 107.969C107 111 107 111 105 114C104.67 107.73 104.34 101.46 104 95Z'
|
||||
fill='#120F05'
|
||||
/>
|
||||
<path
|
||||
d='M56 103C58.6048 102.919 61.2071 102.86 63.8125 102.812C64.5505 102.787 65.2885 102.762 66.0488 102.736C71.4975 102.662 71.4975 102.662 74 104.344C75.374 106.619 75.2112 108.396 75 111C74.34 111 73.68 111 73 111C72.7112 110.196 72.4225 109.391 72.125 108.562C71.2674 105.867 71.2674 105.867 69 105C66.7956 104.77 64.5861 104.589 62.375 104.438C61.1865 104.354 59.998 104.27 58.7734 104.184C57.4006 104.093 57.4006 104.093 56 104C56 103.67 56 103.34 56 103Z'
|
||||
fill='#101010'
|
||||
/>
|
||||
<path
|
||||
d='M23 40C23.66 40 24.32 40 25 40C27.3084 46.3482 27.1982 52.2948 27 59C26.67 59 26.34 59 26 59C25.01 52.73 24.02 46.46 23 40Z'
|
||||
fill='#191409'
|
||||
/>
|
||||
<path
|
||||
d='M47 83C46.3606 83.3094 45.7212 83.6187 45.0625 83.9375C41.9023 87.0977 42.181 90.6833 42 95C41.01 94.67 40.02 94.34 39 94C39.3463 85.7409 39.3463 85.7409 41.875 82.875C44 82 44 82 47 83Z'
|
||||
fill='#171717'
|
||||
/>
|
||||
<path
|
||||
d='M53 61C53.33 61 53.66 61 54 61C54.33 67.93 54.66 74.86 55 82C54.01 82 53.02 82 52 82C52.33 75.07 52.66 68.14 53 61Z'
|
||||
fill='#444444'
|
||||
/>
|
||||
<path
|
||||
d='M81 154C78.6696 156.33 77.8129 156.39 74.625 156.75C73.4687 156.897 73.4687 156.897 72.2891 157.047C69.6838 156.994 68.2195 156.317 66 155C67.7478 154.635 69.4984 154.284 71.25 153.938C72.7118 153.642 72.7118 153.642 74.2031 153.34C76.8681 153.016 78.4887 153.145 81 154Z'
|
||||
fill='#332F23'
|
||||
/>
|
||||
<path
|
||||
d='M19 28C19.66 28 20.32 28 21 28C21.6735 29.4343 22.3386 30.8726 23 32.3125C23.5569 33.5133 23.5569 33.5133 24.125 34.7383C25 37 25 37 25 40C22 39 22 39 21.0508 37.2578C20.8071 36.554 20.5635 35.8502 20.3125 35.125C20.0611 34.4263 19.8098 33.7277 19.5508 33.0078C19 31 19 31 19 28Z'
|
||||
fill='#282213'
|
||||
/>
|
||||
<path
|
||||
d='M102 87C104.429 93.2857 104.429 93.2857 103 97C100.437 94.75 100.437 94.75 98 92C98.0625 89.75 98.0625 89.75 99 88C101 87 101 87 102 87Z'
|
||||
fill='#37301F'
|
||||
/>
|
||||
<path
|
||||
d='M53 56C53.33 56 53.66 56 54 56C53.67 62.27 53.34 68.54 53 75C52.67 75 52.34 75 52 75C51.7788 72.2088 51.5726 69.4179 51.375 66.625C51.3105 65.8309 51.2461 65.0369 51.1797 64.2188C51.0394 62.1497 51.0124 60.0737 51 58C51.66 57.34 52.32 56.68 53 56Z'
|
||||
fill='#030303'
|
||||
/>
|
||||
<path
|
||||
d='M100 129C100.33 129 100.66 129 101 129C100.532 133.776 99.7567 137.045 97 141C96.34 140.67 95.68 140.34 95 140C96.65 136.37 98.3 132.74 100 129Z'
|
||||
fill='#1E1A12'
|
||||
/>
|
||||
<path
|
||||
d='M15 131C17.7061 132.353 17.9618 133.81 19.125 136.562C19.4782 137.389 19.8314 138.215 20.1953 139.066C20.4609 139.704 20.7264 140.343 21 141C20.01 141 19.02 141 18 141C15.9656 137.27 15 135.331 15 131Z'
|
||||
fill='#1C1912'
|
||||
/>
|
||||
<path
|
||||
d='M63 49C69.4 49.4923 69.4 49.4923 72.4375 52.0625C73.2109 53.0216 73.2109 53.0216 74 54C70.8039 54 69.5828 53.4533 66.8125 52C66.0971 51.6288 65.3816 51.2575 64.6445 50.875C64.1018 50.5863 63.5591 50.2975 63 50C63 49.67 63 49.34 63 49Z'
|
||||
fill='#13110C'
|
||||
/>
|
||||
<path
|
||||
d='M0.999992 39C1.98999 39 2.97999 39 3.99999 39C5.24999 46.625 5.24999 46.625 2.99999 50C2.33999 46.37 1.67999 42.74 0.999992 39Z'
|
||||
fill='#312C1E'
|
||||
/>
|
||||
<path
|
||||
d='M94 5C94.66 5 95.32 5 96 5C97.8041 7.75924 98.0127 8.88972 97.625 12.25C97.4187 13.1575 97.2125 14.065 97 15C95.1161 11.7345 94.5071 8.71888 94 5Z'
|
||||
fill='#292417'
|
||||
/>
|
||||
<path
|
||||
d='M20 141C23.3672 142.393 24.9859 143.979 27 147C24.625 146.812 24.625 146.812 22 146C20.6875 143.438 20.6875 143.438 20 141Z'
|
||||
fill='#373328'
|
||||
/>
|
||||
<path
|
||||
d='M86 83C86.33 83.99 86.66 84.98 87 86C83.37 85.34 79.74 84.68 76 84C80.3553 81.8223 81.4663 81.9696 86 83Z'
|
||||
fill='#2F2F2F'
|
||||
/>
|
||||
<path
|
||||
d='M42 93C46 97.625 46 97.625 46 101C44.02 99.35 42.04 97.7 40 96C40.66 95.67 41.32 95.34 42 95C42 94.34 42 93.68 42 93Z'
|
||||
fill='#232323'
|
||||
/>
|
||||
<path
|
||||
d='M34 55C34.66 55.33 35.32 55.66 36 56C35.5256 56.7838 35.0512 57.5675 34.5625 58.375C33.661 59.8895 32.7882 61.4236 32 63C31.34 63 30.68 63 30 63C30.4983 59.3125 31.1007 57.3951 34 55Z'
|
||||
fill='#110F0A'
|
||||
d='M29 7.54605C29 9.47222 28.316 11.1378 26.9481 12.5428C25.5802 13.9251 23.5852 14.9222 20.9634 15.534C22.377 15.9192 23.4484 16.5537 24.1781 17.4375C24.9077 18.2987 25.2724 19.2956 25.2724 20.4287C25.2724 22.2189 24.7025 23.7713 23.5625 25.0855C22.4454 26.3998 20.8039 27.4195 18.638 28.1447C16.4721 28.8472 13.8616 29.1985 10.8066 29.1985C9.66666 29.1985 8.75472 29.1645 8.07075 29.0965C8.04796 29.7309 7.77438 30.2068 7.25 30.5241C6.72562 30.8414 6.05307 31 5.23231 31C4.41156 31 3.84159 30.8187 3.52241 30.4561C3.22603 30.0936 3.10062 29.561 3.14623 28.8586C3.35141 25.686 3.75039 22.3662 4.34316 18.8991C4.93593 15.4094 5.68829 12.0442 6.60024 8.80373C6.75982 8.23721 7.07901 7.84064 7.55778 7.61404C8.03656 7.38743 8.66353 7.27412 9.43868 7.27412C10.8294 7.27412 11.5248 7.65936 11.5248 8.42983C11.5248 8.74708 11.4564 9.10965 11.3196 9.51754C10.7268 11.2851 10.134 13.6871 9.54127 16.7237C8.9485 19.7375 8.52674 22.6156 8.27594 25.3575C9.37028 25.448 10.2594 25.4934 10.9434 25.4934C14.1352 25.4934 16.4721 25.0401 17.954 24.1338C19.4587 23.2046 20.2111 22.0263 20.2111 20.5987C20.2111 19.6016 19.778 18.7632 18.9116 18.0833C18.0681 17.4035 16.6431 17.0296 14.6368 16.9616C14.1808 16.939 13.8616 16.8257 13.6792 16.6217C13.4968 16.4178 13.4057 16.0892 13.4057 15.636C13.4057 14.9788 13.5425 14.4463 13.816 14.0384C14.0896 13.6305 14.5912 13.4152 15.3208 13.3925C16.9395 13.3472 18.3986 13.1093 19.6981 12.6787C21.0204 12.2482 22.0578 11.6477 22.8101 10.8772C23.5625 10.0841 23.9387 9.1663 23.9387 8.1239C23.9387 6.80958 23.2889 5.77851 21.9894 5.0307C20.6899 4.26024 18.6949 3.875 16.0047 3.875C13.5652 3.875 11.2056 4.19226 8.92571 4.82676C6.64584 5.4386 4.70793 6.2204 3.11203 7.17215C2.38246 7.6027 1.7669 7.81798 1.26533 7.81798C0.854953 7.81798 0.53577 7.68202 0.307783 7.41009C0.102594 7.1155 0 6.75292 0 6.32237C0 5.75585 0.113994 5.26864 0.341981 4.86075C0.592768 4.45285 1.17414 3.98831 2.08608 3.46711C4.00118 2.37939 6.24685 1.52961 8.82311 0.917763C11.3994 0.305921 14.0326 0 16.7229 0C20.8494 0 23.9272 0.691156 25.9564 2.07347C27.9855 3.45577 29 5.27998 29 7.54605Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -1861,6 +1983,14 @@ export function Mem0Icon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function EvernoteIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='#7fce2c'>
|
||||
<path d='M29.343 16.818c.1 1.695-.08 3.368-.305 5.045-.225 1.712-.508 3.416-.964 5.084-.3 1.067-.673 2.1-1.202 3.074-.65 1.192-1.635 1.87-2.992 1.924l-3.832.036c-.636-.017-1.278-.146-1.9-.297-1.192-.3-1.862-1.1-2.06-2.3-.186-1.08-.173-2.187.04-3.264.252-1.23 1-1.96 2.234-2.103.817-.1 1.65-.077 2.476-.1.205-.007.275.098.203.287-.196.53-.236 1.07-.098 1.623.053.207-.023.307-.26.305a7.77 7.77 0 0 0-1.123.053c-.636.086-.96.47-.96 1.112 0 .205.026.416.066.622.103.507.45.78.944.837 1.123.127 2.247.138 3.37-.05.675-.114 1.08-.54 1.16-1.208.152-1.3.155-2.587-.228-3.845-.33-1.092-1.006-1.565-2.134-1.7l-3.36-.54c-1.06-.193-1.7-.887-1.92-1.9-.13-.572-.14-1.17-.214-1.757-.013-.106-.074-.208-.1-.3-.04.1-.106.212-.117.326-.066.68-.053 1.373-.185 2.04-.16.8-.404 1.566-.67 2.33-.185.535-.616.837-1.205.8a37.76 37.76 0 0 1-7.123-1.353l-.64-.207c-.927-.26-1.487-.903-1.74-1.787l-1-3.853-.74-4.3c-.115-.755-.2-1.523-.083-2.293.154-1.112.914-1.903 2.04-1.964l3.558-.062c.127 0 .254.003.373-.026a1.23 1.23 0 0 0 1.01-1.255l-.05-3.036c-.048-1.576.8-2.38 2.156-2.622a10.58 10.58 0 0 1 4.91.26c.933.275 1.467.923 1.715 1.83.058.22.146.3.37.287l2.582.01 3.333.37c.686.095 1.364.25 2.032.42 1.165.298 1.793 1.112 1.962 2.256l.357 3.355.3 5.577.01 2.277zm-4.534-1.155c-.02-.666-.07-1.267-.444-1.784a1.66 1.66 0 0 0-2.469-.15c-.364.4-.494.88-.564 1.4-.008.034.106.126.16.126l.8-.053c.768.007 1.523.113 2.25.393.066.026.136.04.265.077zM8.787 1.154a3.82 3.82 0 0 0-.278 1.592l.05 2.934c.005.357-.075.45-.433.45L5.1 6.156c-.583 0-1.143.1-1.554.278l5.2-5.332c.02.013.04.033.06.053z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ElevenLabsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1877,15 +2007,31 @@ export function ElevenLabsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function FathomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 1000' fill='none'>
|
||||
<path
|
||||
d='M0,668.7v205.78c0,53.97,34.24,102.88,85.8,119.08,87.48,27.49,167.88-36.99,167.88-120.22v-77.45L0,668.7Z'
|
||||
fill='#007299'
|
||||
/>
|
||||
<path
|
||||
d='M873.72,626.07c-19.05,0-38.38-4.3-56.58-13.38L72.78,241.43C11.15,210.69-17.51,136.6,11.18,74.05,41.2,8.59,119.26-18.53,183.23,13.38l744.25,371.21c62.45,31.15,91,109.08,59.79,171.43-22.22,44.38-67.02,70.05-113.55,70.05Z'
|
||||
fill='#00beff'
|
||||
/>
|
||||
<path
|
||||
d='M500.09,813.66c-19.05,0-38.38-4.3-56.58-13.38l-370.72-184.9c-61.63-30.74-90.29-104.82-61.61-167.37,30.02-65.46,108.08-92.59,172.06-60.68l370.62,184.85c62.45,31.15,91,109.08,59.79,171.43-22.22,44.38-67.02,70.05-113.55,70.05Z'
|
||||
fill='#00beff'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export function LinkupIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'>
|
||||
<g transform='translate(12, 12) scale(1.3) translate(-12, -12)'>
|
||||
<path
|
||||
d='M20.2 14.1c-.4-.3-1.6-.4-2.9-.2.5-1.4 1.3-3.9.1-5-.6-.5-1.5-.7-2.6-.5-.3 0-.6.1-1 .2-1.1-1.6-2.4-2.5-3.8-2.5-1.6 0-3.1 1-4.1 2.9-1.2 2.1-1.9 5.1-1.9 8.8v.03l.4.3c3-.9 7.5-2.3 10.7-2.9 0 .9.1 1.9.1 2.8v.03l.4.3c.1 0 5.4-1.7 5.3-3.3 0-.2-.1-.5-.3-.7zM19.9 14.7c.03.4-1.7 1.4-4 2.3.5-.7 1-1.6 1.3-2.5 1.4-.1 2.4-.1 2.7.2zM16.4 14.6c-.3.7-.7 1.4-1.2 2-.02-.6-.1-1.2-.2-1.8.4-.1.9-.1 1.4-.2zM16.5 9.4c.8.7.9 2.4.1 5.1-.5.1-1 .1-1.5.2-.3-2-.9-3.8-1.7-5.3.3-.1.6-.2.8-.2.9-.1 1.7.05 2.3.2zM9.5 6.8c1.2 0 2.3.7 3.2 2.1-2.8 1.1-5.9 3.4-8.4 7.8.2-5.1 1.9-9.9 5.2-9.9zM4.7 17c3.4-4.9 6.4-6.8 8.4-7.8.7 1.3 1.2 2.9 1.5 4.8-3.2.6-7.3 1.8-9.9 3z'
|
||||
fill='#000000'
|
||||
/>
|
||||
</g>
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 154 107' fill='none'>
|
||||
<path
|
||||
d='M150.677 72.7113C146.612 70.2493 137.909 69.542 124.794 70.6076C128.992 57.6776 133.757 35.3911 121.323 25.1527C115.886 20.6743 107.471 19.0437 97.6162 20.5594C94.6758 21.0142 91.5752 21.7445 88.3878 22.732C78.8667 8.28165 66.2954 0 53.8613 0C39.4288 0 26.1304 9.3381 16.4081 26.2872C5.67515 45.014 0 71.9626 0 104.23V104.533L3.60356 106.94L3.88251 106.825C30.5754 95.5628 67.5759 85.0718 100.593 79.4037C101.604 87.644 102.116 95.9945 102.116 104.235V104.52L105.491 107L105.761 106.913C106.255 106.752 155.159 90.8822 153.979 77.5894C153.856 76.2022 153.183 74.2271 150.677 72.7113ZM148.409 78.09C148.715 81.5442 133.236 91.0568 111.838 98.8883C115.968 92.0995 119.818 84.1715 122.777 76.3584C135.659 75.1411 144.531 75.5545 147.792 77.5296C148.377 77.8833 148.409 78.09 148.409 78.09ZM116.668 77.0106C114.084 83.3769 110.951 89.6329 107.54 95.2458C107.334 89.5135 106.913 83.8821 106.296 78.4621C109.922 77.8971 113.407 77.4102 116.668 77.0106ZM117.774 29.4979C125.379 35.7585 125.782 51.3205 118.867 71.1772C114.747 71.6319 110.284 72.2382 105.596 72.9777C103.049 55.1742 98.2839 39.966 91.4243 27.7525C94.566 26.8155 96.9669 26.3469 98.4622 26.1127C106.721 24.8404 113.581 26.0438 117.774 29.4979ZM53.8567 5.62215C65.0561 5.62215 74.8882 12.0022 83.0922 24.5923C57.7027 34.5413 30.3193 59.4092 5.78032 94.8003C7.43119 51.4813 23.0299 5.62215 53.8613 5.62215M10.1933 98.2406C40.7504 53.9341 68.2024 36.4429 86.0739 29.5852C92.4487 41.2383 97.2046 56.5522 99.8433 73.9331C70.5209 79.0316 35.6377 88.4983 10.1933 98.2406Z'
|
||||
fill='#000000'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1950,7 +2096,7 @@ export function LangsmithIcon(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
export function LemlistIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 181' fill='none'>
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='24 24.92 132 132' fill='none'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
@@ -2394,6 +2540,17 @@ export function OutlookIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function PagerDutyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64' fill='none'>
|
||||
<path
|
||||
d='M6.704 59.217H0v-33.65c0-3.455 1.418-5.544 2.604-6.704 2.63-2.58 6.2-2.656 6.782-2.656h10.546c3.765 0 5.93 1.52 7.117 2.8 2.346 2.553 2.372 5.853 2.32 6.73v12.687c0 3.662-1.496 5.828-2.733 6.988-2.553 2.398-5.93 2.45-6.73 2.424H6.704zm13.46-18.102c.36 0 1.367-.103 1.908-.62.413-.387.62-1.083.62-2.1v-13.02c0-.36-.077-1.315-.593-1.857-.5-.516-1.444-.62-2.166-.62h-10.6c-2.63 0-2.63 1.985-2.63 2.656v15.55zM57.296 4.783H64V38.46c0 3.455-1.418 5.544-2.604 6.704-2.63 2.58-6.2 2.656-6.782 2.656H44.068c-3.765 0-5.93-1.52-7.117-2.8-2.346-2.553-2.372-5.853-2.32-6.73V25.62c0-3.662 1.496-5.828 2.733-6.988 2.553-2.398 5.93-2.45 6.73-2.424h13.202zM43.836 22.9c-.36 0-1.367.103-1.908.62-.413.387-.62 1.083-.62 2.1v13.02c0 .36.077 1.315.593 1.857.5.516 1.444.62 2.166.62h10.598c2.656-.026 2.656-2 2.656-2.682V22.9z'
|
||||
fill='#FFFFFF'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MicrosoftExcelIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const gradientId = `excel_gradient_${id}`
|
||||
@@ -2928,6 +3085,19 @@ export function QdrantIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function AshbyIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 254 260' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M76.07 250.537v9.16H.343v-9.16c19.618 0 27.465-4.381 34.527-23.498l73.764-209.09h34.92l81.219 209.09c7.847 19.515 11.77 23.498 28.642 23.498v9.16H134.363v-9.16c28.242 0 30.625-2.582 22.14-23.498l-21.58-57.35H69.399l-19.226 56.155c-5.614 18.997-4.387 24.693 25.896 24.693zm24.326-171.653l-26.681 78.459h56.5l-29.819-78.459z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ArxivIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} id='logomark' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 17.732 24.269'>
|
||||
@@ -3430,6 +3600,44 @@ export const ResendIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleAdsIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
|
||||
<g transform='matrix(.257748 0 0 .257745 -.361416 2.515516)'>
|
||||
<path
|
||||
d='M85.9 28.6c2.4-6.3 5.7-12.1 10.6-16.8 19.6-19.1 52-14.3 65.3 9.7 10 18.2 20.6 36 30.9 54l51.6 89.8c14.3 25.1-1.2 56.8-29.6 61.1-17.4 2.6-33.7-5.4-42.7-21l-45.4-78.8c-.3-.6-.7-1.1-1.1-1.6-1.6-1.3-2.3-3.2-3.3-4.9L88.8 62.2c-3.9-6.8-5.7-14.2-5.5-22 .3-4 .8-8 2.6-11.6'
|
||||
fill='#3c8bd9'
|
||||
/>
|
||||
<path
|
||||
d='M85.9 28.6c-.9 3.6-1.7 7.2-1.9 11-.3 8.4 1.8 16.2 6 23.5l32.9 56.9c1 1.7 1.8 3.4 2.8 5l-18.1 31.1-25.3 43.6c-.4 0-.5-.2-.6-.5-.1-.8.2-1.5.4-2.3 4.1-15 .7-28.3-9.6-39.7-6.3-6.9-14.3-10.8-23.5-12.1-12-1.7-22.6 1.4-32.1 8.9-1.7 1.3-2.8 3.2-4.8 4.2-.4 0-.6-.2-.7-.5l14.3-24.9L85.2 29.7c.2-.4.5-.7.7-1.1'
|
||||
fill='#fabc04'
|
||||
/>
|
||||
<path
|
||||
d='M11.8 158l5.7-5.1c24.3-19.2 60.8-5.3 66.1 25.1 1.3 7.3.6 14.3-1.6 21.3-.1.6-.2 1.1-.4 1.7-.9 1.6-1.7 3.3-2.7 4.9-8.9 14.7-22 22-39.2 20.9C20 225.4 4.5 210.6 1.8 191c-1.3-9.5.6-18.4 5.5-26.6 1-1.8 2.2-3.4 3.3-5.2.5-.4.3-1.2 1.2-1.2'
|
||||
fill='#34a852'
|
||||
/>
|
||||
<path d='M11.8 158c-.4.4-.4 1.1-1.1 1.2-.1-.7.3-1.1.7-1.6l.4.4' fill='#fabc04' />
|
||||
<path d='M81.6 201c-.4-.7 0-1.2.4-1.7l.4.4-.8 1.3' fill='#e1c025' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleBigQueryIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
|
||||
<path
|
||||
d='M14.48 58.196L.558 34.082c-.744-1.288-.744-2.876 0-4.164L14.48 5.805c.743-1.287 2.115-2.08 3.6-2.082h27.857c1.48.007 2.845.8 3.585 2.082l13.92 24.113c.744 1.288.744 2.876 0 4.164L49.52 58.196c-.743 1.287-2.115 2.08-3.6 2.082H18.07c-1.483-.005-2.85-.798-3.593-2.082z'
|
||||
fill='#4386fa'
|
||||
/>
|
||||
<path
|
||||
d='M40.697 24.235s3.87 9.283-1.406 14.545-14.883 1.894-14.883 1.894L43.95 60.27h1.984c1.486-.002 2.858-.796 3.6-2.082L58.75 42.23z'
|
||||
opacity='.1'
|
||||
/>
|
||||
<path
|
||||
d='M45.267 43.23L41 38.953a.67.67 0 0 0-.158-.12 11.63 11.63 0 1 0-2.032 2.037.67.67 0 0 0 .113.15l4.277 4.277a.67.67 0 0 0 .947 0l1.12-1.12a.67.67 0 0 0 0-.947zM31.64 40.464a8.75 8.75 0 1 1 8.749-8.749 8.75 8.75 0 0 1-8.749 8.749zm-5.593-9.216v3.616c.557.983 1.363 1.803 2.338 2.375v-6.013zm4.375-2.998v9.772a6.45 6.45 0 0 0 2.338 0V28.25zm6.764 6.606v-2.142H34.85v4.5a6.43 6.43 0 0 0 2.338-2.368z'
|
||||
fill='#fff'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleVaultIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 82 82'>
|
||||
<path
|
||||
@@ -3905,6 +4113,28 @@ export function IntercomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function LoopsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 214 186' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M122.19,0 H90.27 C40.51,0 0,39.88 0,92.95 C0,141.07 38.93,183.77 90.27,183.77 H122.19 C172.61,183.77 213.31,142.82 213.31,92.95 C213.31,43.29 173.09,0 122.19,0 Z M10.82,92.54 C10.82,50.19 45.91,11.49 91.96,11.49 C138.73,11.49 172.69,50.33 172.69,92.13 C172.69,117.76 154.06,139.09 129.02,143.31 C145.16,131.15 155.48,112.73 155.48,92.4 C155.48,59.09 127.44,28.82 92.37,28.82 C57.23,28.82 28.51,57.23 28.51,92.91 C28.51,122.63 43.61,151.08 69.99,168.21 L71.74,169.33 C35.99,161.39 10.82,130.11 10.82,92.54 Z M106.33,42.76 C128.88,50.19 143.91,68.92 143.91,92.26 C143.91,114.23 128.68,134.63 106.12,141.71 C105.44,141.96 105.17,141.96 105.17,141.96 C83.91,135.76 69.29,116.38 69.29,92.71 C69.29,69.91 83.71,50.33 106.33,42.76 Z M120.91,172.13 C76.11,172.13 40.09,137.21 40.09,93.32 C40.09,67.03 57.17,46.11 83.98,41.33 C67.04,53.83 57.3,71.71 57.3,92.71 C57.3,125.75 82.94,155.33 120.77,155.33 C155.01,155.33 184.31,125.2 184.31,92.47 C184.31,62.34 169.96,34.06 141.92,14.55 L141.65,14.34 C175.81,23.68 202.26,54.11 202.26,92.81 C202.26,135.69 166.38,172.13 120.91,172.13 Z'
|
||||
fill='#FB5001'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function LumaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} fill='none' viewBox='0 0 133 134' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M133 67C96.282 67 66.5 36.994 66.5 0c0 36.994-29.782 67-66.5 67 36.718 0 66.5 30.006 66.5 67 0-36.994 29.782-67 66.5-67'
|
||||
fill='#000000'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MailchimpIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -4426,6 +4656,17 @@ export function SSHIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DatabricksIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 241 266' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M228.085 109.654L120.615 171.674L5.53493 105.41L0 108.475V156.582L120.615 225.911L228.085 164.128V189.596L120.615 251.615L5.53493 185.351L0 188.417V196.67L120.615 266L241 196.67V148.564L235.465 145.498L120.615 211.527L12.9148 149.743V124.275L120.615 186.059L241 116.729V69.3298L235.004 65.7925L120.615 131.585L18.4498 73.1028L120.615 14.3848L204.562 62.7269L211.942 58.4823V52.5869L120.615 0L0 69.3298V76.8759L120.615 146.206L228.085 84.1862V109.654Z'
|
||||
fill='#FF3621'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DatadogIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
|
||||
@@ -4654,6 +4895,22 @@ export function GoogleGroupsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleMeetIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 87.5 72'>
|
||||
<path fill='#00832d' d='M49.5 36l8.53 9.75 11.47 7.33 2-17.02-2-16.64-11.69 6.44z' />
|
||||
<path fill='#0066da' d='M0 51.5V66c0 3.315 2.685 6 6 6h14.5l3-10.96-3-9.54-9.95-3z' />
|
||||
<path fill='#e94235' d='M20.5 0L0 20.5l10.55 3 9.95-3 2.95-9.41z' />
|
||||
<path fill='#2684fc' d='M20.5 20.5H0v31h20.5z' />
|
||||
<path
|
||||
fill='#00ac47'
|
||||
d='M82.6 8.68L69.5 19.42v33.66l13.16 10.79c1.97 1.54 4.85.135 4.85-2.37V11c0-2.535-2.945-3.925-4.91-2.32zM49.5 36v15.5h-29V72h43c3.315 0 6-2.685 6-6V53.08z'
|
||||
/>
|
||||
<path fill='#ffba00' d='M63.5 0h-43v20.5h29V36l20-16.57V6c0-3.315-2.685-6-6-6z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CursorIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 546 546' fill='currentColor'>
|
||||
@@ -4662,6 +4919,19 @@ export function CursorIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DubIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 64 64' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M32 64c17.673 0 32-14.327 32-32 0-11.844-6.435-22.186-16-27.719V48h-8v-2.14A15.9 15.9 0 0 1 32 48c-8.837 0-16-7.163-16-16s7.163-16 16-16c2.914 0 5.647.78 8 2.14V1.008A32 32 0 0 0 32 0C14.327 0 0 14.327 0 32s14.327 32 32 32'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DuckDuckGoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='-108 -108 216 216'>
|
||||
@@ -4774,6 +5044,17 @@ export function CirclebackIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GreenhouseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 51.4 107.7' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M44.9,32c0,5.2-2.2,9.8-5.8,13.4c-4,4-9.8,5-9.8,8.4c0,4.6,7.4,3.2,14.5,10.3c4.7,4.7,7.6,10.9,7.6,18.1c0,14.2-11.4,25.5-25.7,25.5S0,96.4,0,82.2C0,75,2.9,68.8,7.6,64.1c7.1-7.1,14.5-5.7,14.5-10.3c0-3.4-5.8-4.4-9.8-8.4c-3.6-3.6-5.8-8.2-5.8-13.6C6.5,21.4,15,13,25.4,13c2,0,3.8,0.3,5.3,0.3c2.7,0,4.1-1.2,4.1-3.1c0-1.1-0.5-2.5-0.5-4c0-3.4,2.9-6.2,6.4-6.2S47,2.9,47,6.4c0,3.7-2.9,5.4-5.1,6.2c-1.8,0.6-3.2,1.4-3.2,3.2C38.7,19.2,44.9,22.5,44.9,32z M42.9,82.2c0-9.9-7.3-17.9-17.2-17.9s-17.2,8-17.2,17.9c0,9.8,7.3,17.9,17.2,17.9S42.9,92,42.9,82.2z M37,31.8c0-6.3-5.1-11.5-11.3-11.5s-11.3,5.2-11.3,11.5s5.1,11.5,11.3,11.5S37,38.1,37,31.8z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GreptileIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||||
@@ -5445,6 +5726,35 @@ export function GoogleMapsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function GooglePagespeedIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='-1.74 -1.81 285.55 266.85' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M272.73 37.23v179.68a18.58 18.58 0 0 1-18.57 18.59H18.65A18.58 18.58 0 0 1 .06 216.94V37.23z'
|
||||
fill='#e1e1e1'
|
||||
/>
|
||||
<path
|
||||
d='M18.65 0h235.5a18.58 18.58 0 0 1 18.58 18.56v18.67H.07V18.59A18.58 18.58 0 0 1 18.64 0z'
|
||||
fill='#c2c2c2'
|
||||
/>
|
||||
<path
|
||||
d='M136.3 92.96a99 99 0 0 0-99 99v.13c0 2.08-.12 4.64 0 6.2h43.25a54.87 54.87 0 0 1 0-6.2 55.81 55.81 0 0 1 85.06-47.45l31.12-31.12a98.76 98.76 0 0 0-60.44-20.57z'
|
||||
fill='#4285f4'
|
||||
/>
|
||||
<path
|
||||
d='M196.73 113.46l-31.14 31.14a55.74 55.74 0 0 1 26.56 47.45 54.87 54.87 0 0 1 0 6.2h43.39c.12-1.48 0-4.12 0-6.2a99 99 0 0 0-38.81-78.59z'
|
||||
fill='#f44336'
|
||||
/>
|
||||
<circle cx='24.85' cy='18.59' fill='#eee' r='6.2' />
|
||||
<circle cx='49.65' cy='18.59' fill='#eee' r='6.2' />
|
||||
<path
|
||||
d='M197.01 117.23a3.05 3.05 0 0 0 .59-1.81 3.11 3.11 0 0 0-3.1-3.1 3 3 0 0 0-1.91.68l-67.56 52a18.58 18.58 0 1 0 27.24 24.33l44.73-72.1z'
|
||||
fill='#9e9e9e'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GoogleTranslateIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 998.1 998.3'>
|
||||
|
||||
@@ -1,53 +1,95 @@
|
||||
'use client'
|
||||
|
||||
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 border-border/50 border-b bg-background/80 backdrop-blur-md backdrop-saturate-150'>
|
||||
{/* Desktop: Single row layout */}
|
||||
<div className='hidden h-16 w-full items-center lg:flex'>
|
||||
<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 */}
|
||||
<div
|
||||
className='relative flex w-full items-center justify-between'
|
||||
className='relative flex h-[52px] w-full items-center justify-between'
|
||||
style={{
|
||||
paddingLeft: 'calc(var(--sidebar-offset) + 32px)',
|
||||
paddingRight: 'calc(var(--toc-offset) + 60px)',
|
||||
}}
|
||||
>
|
||||
{/* Left cluster: logo */}
|
||||
<div className='flex items-center'>
|
||||
<Link href='/' className='flex min-w-[100px] items-center'>
|
||||
<SimLogoFull className='h-7 w-auto' />
|
||||
</Link>
|
||||
</div>
|
||||
<Link href='/' className='flex min-w-[100px] items-center'>
|
||||
<SimLogoFull className='h-7 w-auto' />
|
||||
</Link>
|
||||
|
||||
{/* Center cluster: search - absolutely positioned to center */}
|
||||
<div className='-translate-x-1/2 absolute left-1/2 flex items-center justify-center'>
|
||||
<SearchTrigger />
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className='flex items-center gap-1'>
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -25,8 +25,8 @@ export function StructuredData({
|
||||
headline: title,
|
||||
description: description,
|
||||
url: url,
|
||||
datePublished: dateModified || new Date().toISOString(),
|
||||
dateModified: dateModified || new Date().toISOString(),
|
||||
...(dateModified && { datePublished: dateModified }),
|
||||
...(dateModified && { dateModified }),
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'Sim Team',
|
||||
@@ -74,7 +74,7 @@ export function StructuredData({
|
||||
name: 'Sim Documentation',
|
||||
url: baseUrl,
|
||||
description:
|
||||
'Comprehensive documentation for Sim visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.',
|
||||
'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.',
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'Sim',
|
||||
@@ -91,12 +91,6 @@ 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',
|
||||
@@ -104,7 +98,7 @@ export function StructuredData({
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
operatingSystem: 'Any',
|
||||
description:
|
||||
'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.',
|
||||
'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.',
|
||||
url: baseUrl,
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
@@ -115,12 +109,13 @@ export function StructuredData({
|
||||
category: 'Developer Tools',
|
||||
},
|
||||
featureList: [
|
||||
'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',
|
||||
'AI agent creation',
|
||||
'Agentic workflow orchestration',
|
||||
'1,000+ integrations',
|
||||
'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
|
||||
'Knowledge base creation',
|
||||
'Table creation',
|
||||
'Document creation',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -151,15 +146,6 @@ 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'
|
||||
|
||||
195
apps/docs/components/ui/animated-blocks.tsx
Normal file
195
apps/docs/components/ui/animated-blocks.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { memo } from 'react'
|
||||
|
||||
const RX = '2.59574'
|
||||
|
||||
interface BlockRect {
|
||||
opacity: number
|
||||
width: string
|
||||
height: string
|
||||
fill: string
|
||||
x?: string
|
||||
y?: string
|
||||
transform?: string
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
],
|
||||
bottomLeft: [
|
||||
{ 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',
|
||||
},
|
||||
],
|
||||
bottomRight: [
|
||||
{
|
||||
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: '34.24',
|
||||
fill: '#2ABBF8',
|
||||
transform: 'matrix(-1 0 0 1 33.787 68)',
|
||||
},
|
||||
{
|
||||
opacity: 0.4,
|
||||
width: '16.8626',
|
||||
height: '16.8626',
|
||||
fill: '#1A8FCC',
|
||||
transform: 'matrix(-1 0 0 1 33.787 85)',
|
||||
},
|
||||
],
|
||||
} as const satisfies Record<string, readonly BlockRect[]>
|
||||
|
||||
const GLOBAL_OPACITY = 0.55
|
||||
|
||||
const BlockGroup = memo(function BlockGroup({
|
||||
width,
|
||||
height,
|
||||
viewBox,
|
||||
rects,
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
viewBox: string
|
||||
rects: readonly BlockRect[]
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={viewBox}
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
className='h-auto w-full'
|
||||
style={{ opacity: GLOBAL_OPACITY }}
|
||||
>
|
||||
{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}
|
||||
opacity={r.opacity}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
})
|
||||
|
||||
export function AnimatedBlocks() {
|
||||
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} />
|
||||
</div>
|
||||
|
||||
<div className='-left-24 absolute bottom-0 w-[calc(140px+10.76vw)] max-w-[295px] rotate-180'>
|
||||
<BlockGroup width={295} height={34} viewBox='0 0 295 34' rects={RECTS.bottomLeft} />
|
||||
</div>
|
||||
|
||||
<div className='-bottom-2 absolute right-0 w-[calc(16px+1.25vw)] max-w-[34px]'>
|
||||
<BlockGroup width={34} height={102} viewBox='0 0 34 102' rects={RECTS.bottomRight} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
apps/docs/components/ui/dropdown-menu.tsx
Normal file
73
apps/docs/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'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,
|
||||
}
|
||||
47
apps/docs/components/ui/faq.tsx
Normal file
47
apps/docs/components/ui/faq.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -9,12 +9,15 @@ import {
|
||||
AirtableIcon,
|
||||
AirweaveIcon,
|
||||
AlgoliaIcon,
|
||||
AmplitudeIcon,
|
||||
ApifyIcon,
|
||||
ApolloIcon,
|
||||
ArxivIcon,
|
||||
AsanaIcon,
|
||||
AshbyIcon,
|
||||
AttioIcon,
|
||||
BrainIcon,
|
||||
BrandfetchIcon,
|
||||
BrowserUseIcon,
|
||||
CalComIcon,
|
||||
CalendlyIcon,
|
||||
@@ -24,38 +27,51 @@ import {
|
||||
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,
|
||||
@@ -73,6 +89,8 @@ import {
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
LinkupIcon,
|
||||
LoopsIcon,
|
||||
LumaIcon,
|
||||
MailchimpIcon,
|
||||
MailgunIcon,
|
||||
MailServerIcon,
|
||||
@@ -88,10 +106,12 @@ import {
|
||||
MySQLIcon,
|
||||
Neo4jIcon,
|
||||
NotionIcon,
|
||||
ObsidianIcon,
|
||||
OnePasswordIcon,
|
||||
OpenAIIcon,
|
||||
OutlookIcon,
|
||||
PackageSearchIcon,
|
||||
PagerDutyIcon,
|
||||
ParallelIcon,
|
||||
PerplexityIcon,
|
||||
PineconeIcon,
|
||||
@@ -157,11 +177,14 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
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,
|
||||
@@ -171,37 +194,50 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
cloudflare: CloudflareIcon,
|
||||
confluence_v2: 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,
|
||||
firecrawl: FirecrawlIcon,
|
||||
fireflies_v2: FirefliesIcon,
|
||||
gamma: GammaIcon,
|
||||
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_vault: GoogleVaultIcon,
|
||||
grafana: GrafanaIcon,
|
||||
grain: GrainIcon,
|
||||
greenhouse: GreenhouseIcon,
|
||||
greptile: GreptileIcon,
|
||||
hex: HexIcon,
|
||||
hubspot: HubspotIcon,
|
||||
@@ -221,6 +257,8 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
linear: LinearIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
linkup: LinkupIcon,
|
||||
loops: LoopsIcon,
|
||||
luma: LumaIcon,
|
||||
mailchimp: MailchimpIcon,
|
||||
mailgun: MailgunIcon,
|
||||
mem0: Mem0Icon,
|
||||
@@ -234,10 +272,12 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
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,
|
||||
|
||||
@@ -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-90',
|
||||
enableLightbox && 'cursor-pointer transition-opacity hover:opacity-95',
|
||||
className
|
||||
)}
|
||||
alt={alt}
|
||||
|
||||
@@ -55,8 +55,9 @@ export function Lightbox({ isOpen, onClose, src, alt, type }: LightboxProps) {
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className='max-h-[calc(100vh-6rem)] max-w-[calc(100vw-6rem)] rounded-xl object-contain'
|
||||
className='max-h-[75vh] max-w-[75vw] cursor-pointer rounded-xl object-contain'
|
||||
loading='lazy'
|
||||
onClick={onClose}
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
@@ -65,7 +66,8 @@ export function Lightbox({ isOpen, onClose, src, alt, type }: LightboxProps) {
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className='max-h-[calc(100vh-6rem)] max-w-[calc(100vw-6rem)] rounded-xl outline-none focus:outline-none'
|
||||
className='max-h-[75vh] max-w-[75vw] cursor-pointer rounded-xl outline-none focus:outline-none'
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
169
apps/docs/components/ui/response-section.tsx
Normal file
169
apps/docs/components/ui/response-section.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -15,23 +15,14 @@ export function SearchTrigger() {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
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)',
|
||||
}}
|
||||
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'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Search className='h-4 w-4' />
|
||||
<Search className='h-3.5 w-3.5' />
|
||||
<span>Search...</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 className='ml-auto flex items-center font-medium'>
|
||||
<span className='text-[15px]'>⌘</span>
|
||||
<span className='text-[12px]'>K</span>
|
||||
</kbd>
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ export function Video({
|
||||
loop={loop}
|
||||
muted={muted}
|
||||
playsInline={playsInline}
|
||||
className={`${className} ${enableLightbox ? 'cursor-pointer transition-opacity hover:opacity-90' : ''}`}
|
||||
className={`${className} ${enableLightbox ? 'cursor-pointer transition-opacity hover:opacity-95' : ''}`}
|
||||
src={getAssetUrl(src)}
|
||||
onClick={handleVideoClick}
|
||||
/>
|
||||
|
||||
94
apps/docs/content/docs/de/api-reference/authentication.mdx
Normal file
94
apps/docs/content/docs/de/api-reference/authentication.mdx
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
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>
|
||||
210
apps/docs/content/docs/de/api-reference/getting-started.mdx
Normal file
210
apps/docs/content/docs/de/api-reference/getting-started.mdx
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
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.
|
||||
18
apps/docs/content/docs/de/api-reference/meta.json
Normal file
18
apps/docs/content/docs/de/api-reference/meta.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
766
apps/docs/content/docs/de/api-reference/python.mdx
Normal file
766
apps/docs/content/docs/de/api-reference/python.mdx
Normal file
@@ -0,0 +1,766 @@
|
||||
---
|
||||
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)
|
||||
```
|
||||
|
||||
### Umgebungskonfiguration
|
||||
|
||||
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
|
||||
1052
apps/docs/content/docs/de/api-reference/typescript.mdx
Normal file
1052
apps/docs/content/docs/de/api-reference/typescript.mdx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -190,13 +190,8 @@ console.log(`${processedItems} gültige Elemente verarbeitet`);
|
||||
|
||||
### Einschränkungen
|
||||
|
||||
<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 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>
|
||||
|
||||
<Callout type="info">
|
||||
|
||||
@@ -142,11 +142,8 @@ Jede parallele Instanz läuft unabhängig:
|
||||
|
||||
### Einschränkungen
|
||||
|
||||
<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 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>
|
||||
|
||||
<Callout type="info">
|
||||
|
||||
24
apps/docs/content/docs/de/meta.json
Normal file
24
apps/docs/content/docs/de/meta.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"pages": ["executeWorkflow", "cancelExecution", "listWorkflows", "getWorkflow", "getJobStatus"]
|
||||
}
|
||||
94
apps/docs/content/docs/en/api-reference/authentication.mdx
Normal file
94
apps/docs/content/docs/en/api-reference/authentication.mdx
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
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>
|
||||
210
apps/docs/content/docs/en/api-reference/getting-started.mdx
Normal file
210
apps/docs/content/docs/en/api-reference/getting-started.mdx
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
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 platform and navigate to **Settings**, then go to **Sim Keys** and 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, or use the dashboard to manage deployments.
|
||||
</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.
|
||||
19
apps/docs/content/docs/en/api-reference/meta.json
Normal file
19
apps/docs/content/docs/en/api-reference/meta.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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",
|
||||
"(generated)/knowledge-bases"
|
||||
]
|
||||
}
|
||||
761
apps/docs/content/docs/en/api-reference/python.mdx
Normal file
761
apps/docs/content/docs/en/api-reference/python.mdx
Normal file
@@ -0,0 +1,761 @@
|
||||
---
|
||||
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'
|
||||
|
||||
The official Python SDK for Sim allows you to execute workflows programmatically from your Python applications using the official Python SDK.
|
||||
|
||||
<Callout type="info">
|
||||
The Python SDK supports Python 3.8+ with async execution support, automatic rate limiting with exponential backoff, and usage tracking.
|
||||
</Callout>
|
||||
|
||||
## Installation
|
||||
|
||||
Install the SDK using pip:
|
||||
|
||||
```bash
|
||||
pip install simstudio-sdk
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Here's a simple example to get you started:
|
||||
|
||||
```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 Reference
|
||||
|
||||
### SimStudioClient
|
||||
|
||||
#### Constructor
|
||||
|
||||
```python
|
||||
SimStudioClient(api_key: str, base_url: str = "https://sim.ai")
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `api_key` (str): Your Sim API key
|
||||
- `base_url` (str, optional): Base URL for the Sim API
|
||||
|
||||
#### Methods
|
||||
|
||||
##### execute_workflow()
|
||||
|
||||
Execute a workflow with optional input data.
|
||||
|
||||
```python
|
||||
result = client.execute_workflow(
|
||||
"workflow-id",
|
||||
input_data={"message": "Hello, world!"},
|
||||
timeout=30.0 # 30 seconds
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflow_id` (str): The ID of the workflow to execute
|
||||
- `input_data` (dict, optional): Input data to pass to the workflow
|
||||
- `timeout` (float, optional): Timeout in seconds (default: 30.0)
|
||||
- `stream` (bool, optional): Enable streaming responses (default: False)
|
||||
- `selected_outputs` (list[str], optional): Block outputs to stream in `blockName.attribute` format (e.g., `["agent1.content"]`)
|
||||
- `async_execution` (bool, optional): Execute asynchronously (default: False)
|
||||
|
||||
**Returns:** `WorkflowExecutionResult | AsyncExecutionResult`
|
||||
|
||||
When `async_execution=True`, returns immediately with a task ID for polling. Otherwise, waits for completion.
|
||||
|
||||
##### get_workflow_status()
|
||||
|
||||
Get the status of a workflow (deployment status, etc.).
|
||||
|
||||
```python
|
||||
status = client.get_workflow_status("workflow-id")
|
||||
print("Is deployed:", status.is_deployed)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflow_id` (str): The ID of the workflow
|
||||
|
||||
**Returns:** `WorkflowStatus`
|
||||
|
||||
##### validate_workflow()
|
||||
|
||||
Validate that a workflow is ready for execution.
|
||||
|
||||
```python
|
||||
is_ready = client.validate_workflow("workflow-id")
|
||||
if is_ready:
|
||||
# Workflow is deployed and ready
|
||||
pass
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflow_id` (str): The ID of the workflow
|
||||
|
||||
**Returns:** `bool`
|
||||
|
||||
##### get_job_status()
|
||||
|
||||
Get the status of an async job execution.
|
||||
|
||||
```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"])
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `task_id` (str): The task ID returned from async execution
|
||||
|
||||
**Returns:** `Dict[str, Any]`
|
||||
|
||||
**Response fields:**
|
||||
- `success` (bool): Whether the request was successful
|
||||
- `taskId` (str): The task ID
|
||||
- `status` (str): One of `'queued'`, `'processing'`, `'completed'`, `'failed'`, `'cancelled'`
|
||||
- `metadata` (dict): Contains `startedAt`, `completedAt`, and `duration`
|
||||
- `output` (any, optional): The workflow output (when completed)
|
||||
- `error` (any, optional): Error details (when failed)
|
||||
- `estimatedDuration` (int, optional): Estimated duration in milliseconds (when processing/queued)
|
||||
|
||||
##### execute_with_retry()
|
||||
|
||||
Execute a workflow with automatic retry on rate limit errors using exponential backoff.
|
||||
|
||||
```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
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflow_id` (str): The ID of the workflow to execute
|
||||
- `input_data` (dict, optional): Input data to pass to the workflow
|
||||
- `timeout` (float, optional): Timeout in seconds
|
||||
- `stream` (bool, optional): Enable streaming responses
|
||||
- `selected_outputs` (list, optional): Block outputs to stream
|
||||
- `async_execution` (bool, optional): Execute asynchronously
|
||||
- `max_retries` (int, optional): Maximum number of retries (default: 3)
|
||||
- `initial_delay` (float, optional): Initial delay in seconds (default: 1.0)
|
||||
- `max_delay` (float, optional): Maximum delay in seconds (default: 30.0)
|
||||
- `backoff_multiplier` (float, optional): Backoff multiplier (default: 2.0)
|
||||
|
||||
**Returns:** `WorkflowExecutionResult | AsyncExecutionResult`
|
||||
|
||||
The retry logic uses exponential backoff (1s → 2s → 4s → 8s...) with ±25% jitter to prevent thundering herd. If the API provides a `retry-after` header, it will be used instead.
|
||||
|
||||
##### get_rate_limit_info()
|
||||
|
||||
Get the current rate limit information from the last API response.
|
||||
|
||||
```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))
|
||||
```
|
||||
|
||||
**Returns:** `RateLimitInfo | None`
|
||||
|
||||
##### get_usage_limits()
|
||||
|
||||
Get current usage limits and quota information for your account.
|
||||
|
||||
```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"])
|
||||
```
|
||||
|
||||
**Returns:** `UsageLimits`
|
||||
|
||||
**Response structure:**
|
||||
```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()
|
||||
|
||||
Update the API key.
|
||||
|
||||
```python
|
||||
client.set_api_key("new-api-key")
|
||||
```
|
||||
|
||||
##### set_base_url()
|
||||
|
||||
Update the base URL.
|
||||
|
||||
```python
|
||||
client.set_base_url("https://my-custom-domain.com")
|
||||
```
|
||||
|
||||
##### close()
|
||||
|
||||
Close the underlying HTTP session.
|
||||
|
||||
```python
|
||||
client.close()
|
||||
```
|
||||
|
||||
## Data Classes
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
**Common error codes:**
|
||||
- `UNAUTHORIZED`: Invalid API key
|
||||
- `TIMEOUT`: Request timed out
|
||||
- `RATE_LIMIT_EXCEEDED`: Rate limit exceeded
|
||||
- `USAGE_LIMIT_EXCEEDED`: Usage limit exceeded
|
||||
- `EXECUTION_ERROR`: Workflow execution failed
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Workflow Execution
|
||||
|
||||
<Steps>
|
||||
<Step title="Initialize the client">
|
||||
Set up the SimStudioClient with your API key.
|
||||
</Step>
|
||||
<Step title="Validate the workflow">
|
||||
Check if the workflow is deployed and ready for execution.
|
||||
</Step>
|
||||
<Step title="Execute the workflow">
|
||||
Run the workflow with your input data.
|
||||
</Step>
|
||||
<Step title="Handle the result">
|
||||
Process the execution result and handle any errors.
|
||||
</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()
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Handle different types of errors that may occur during workflow execution:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Context Manager Usage
|
||||
|
||||
Use the client as a context manager to automatically handle resource cleanup:
|
||||
|
||||
```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 Execution
|
||||
|
||||
Execute multiple workflows efficiently:
|
||||
|
||||
```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'}")
|
||||
```
|
||||
|
||||
### Async Workflow Execution
|
||||
|
||||
Execute workflows asynchronously for long-running tasks:
|
||||
|
||||
```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()
|
||||
```
|
||||
|
||||
### Rate Limiting and Retry
|
||||
|
||||
Handle rate limits automatically with exponential 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()
|
||||
```
|
||||
|
||||
### Usage Monitoring
|
||||
|
||||
Monitor your account usage and limits:
|
||||
|
||||
```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 Execution
|
||||
|
||||
Execute workflows with real-time streaming responses:
|
||||
|
||||
```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()
|
||||
```
|
||||
|
||||
The streaming response follows the 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 Example:**
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
Configure the client using environment variables:
|
||||
|
||||
<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>
|
||||
|
||||
## Getting Your API Key
|
||||
|
||||
<Steps>
|
||||
<Step title="Log in to Sim">
|
||||
Navigate to [Sim](https://sim.ai) and log in to your account.
|
||||
</Step>
|
||||
<Step title="Open your workflow">
|
||||
Navigate to the workflow you want to execute programmatically.
|
||||
</Step>
|
||||
<Step title="Deploy your workflow">
|
||||
Click on "Deploy" to deploy your workflow if it hasn't been deployed yet.
|
||||
</Step>
|
||||
<Step title="Create or select an API key">
|
||||
During the deployment process, select or create an API key.
|
||||
</Step>
|
||||
<Step title="Copy the API key">
|
||||
Copy the API key to use in your Python application.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- requests >= 2.25.0
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
1035
apps/docs/content/docs/en/api-reference/typescript.mdx
Normal file
1035
apps/docs/content/docs/en/api-reference/typescript.mdx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ title: Agent
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Agent block connects your workflow to Large Language Models (LLMs). It processes natural language inputs, calls external tools, and generates structured or unstructured outputs.
|
||||
|
||||
@@ -58,7 +59,7 @@ Controls response randomness and creativity:
|
||||
|
||||
### Max Output Tokens
|
||||
|
||||
Controls the maximum length of the model's response. For Anthropic models, Sim uses reliable defaults: streaming executions use the model's full capacity (e.g. 64,000 tokens for Claude 4.5), while non-streaming executions default to 8,192 to avoid timeout issues. When using tools with Anthropic models, intermediate tool-calling requests use a capped limit of 8,192 tokens to avoid SDK timeout errors, regardless of your configured max tokens—the final streaming response uses your full configured limit. This only affects Anthropic's direct API; AWS Bedrock handles this automatically. For long-form content generation via API, explicitly set a higher value.
|
||||
Controls the maximum length of the model's response. Each model defaults to its full max output token limit (e.g., 64,000 tokens for Claude Sonnet 4.5). You can override this with a custom value using the Max Tokens setting. For Anthropic models, when non-streaming requests exceed the SDK's internal threshold, the provider automatically uses internal streaming to avoid timeouts.
|
||||
|
||||
### API Key
|
||||
|
||||
@@ -78,7 +79,7 @@ Extend agent capabilities with external integrations. Select from 60+ pre-built
|
||||
**Execution Modes:**
|
||||
- **Auto**: Model decides when to use tools based on context
|
||||
- **Required**: Tool must be called in every request
|
||||
- **None**: Tool available but not suggested to model
|
||||
- **None**: Tool is completely filtered out and not sent to the model — effectively disables the tool
|
||||
|
||||
### Response Format
|
||||
|
||||
@@ -113,7 +114,7 @@ After an agent completes, you can access its outputs:
|
||||
|
||||
- **`<agent.content>`**: The agent's response text or structured data
|
||||
- **`<agent.tokens>`**: Token usage statistics (prompt, completion, total)
|
||||
- **`<agent.tool_calls>`**: Details of any tools the agent used during execution
|
||||
- **`<agent.toolCalls>`**: Details of any tools the agent used during execution
|
||||
- **`<agent.cost>`**: Estimated cost of the API call (if available)
|
||||
|
||||
## Advanced Features
|
||||
@@ -131,8 +132,9 @@ See the [`Memory`](/tools/memory) block reference for details.
|
||||
## Outputs
|
||||
|
||||
- **`<agent.content>`**: Agent's response text
|
||||
- **`<agent.model>`**: Model identifier used for the request
|
||||
- **`<agent.tokens>`**: Token usage statistics
|
||||
- **`<agent.tool_calls>`**: Tool execution details
|
||||
- **`<agent.toolCalls>`**: Tool execution details
|
||||
- **`<agent.cost>`**: Estimated API call cost
|
||||
|
||||
## Example Use Cases
|
||||
@@ -157,3 +159,13 @@ Input → Agent (Google Search, Notion) → Function (Compile Report)
|
||||
- **Be specific in system prompts**: Clearly define the agent's role, tone, and limitations. The more specific your instructions are, the better the agent will be able to fulfill its intended purpose.
|
||||
- **Choose the right temperature setting**: Use lower temperature settings (0-0.3) when accuracy is important, or increase temperature (0.7-2.0) for more creative or varied responses
|
||||
- **Leverage tools effectively**: Integrate tools that complement the agent's purpose and enhance its capabilities. Be selective about which tools you provide to avoid overwhelming the agent. For tasks with little overlap, use another Agent block for the best results.
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What LLM providers does the Agent block support?", answer: "The Agent block supports OpenAI, Anthropic, Google (Gemini), xAI (Grok), DeepSeek, Groq, Cerebras, Azure OpenAI, Azure Anthropic, Google Vertex AI, AWS Bedrock, OpenRouter, and local models via Ollama or VLLM. You can type or select any supported model from the model combobox." },
|
||||
{ question: "What are the memory options for the Agent block?", answer: "The Agent block has four memory modes: None (no memory, each request is independent), Conversation (full conversation history keyed by a conversation ID), Sliding Window by messages (keeps the N most recent messages), and Sliding Window by tokens (keeps messages up to a token limit). Memory requires a conversation ID to persist across runs." },
|
||||
{ question: "What is the difference between the tool execution modes (Auto, Required, None)?", answer: "In Auto mode, the model decides when to call a tool based on context. In Required mode, the model must call the tool on every request. In None mode, the tool is completely filtered out and never sent to the model — it effectively disables that tool without removing it from the configuration." },
|
||||
{ question: "How does the Response Format work?", answer: "Response Format enforces structured output by providing a JSON Schema. When set, the model's response is constrained to match the schema exactly. Fields from the structured response can be accessed directly by downstream blocks using <agent.fieldName> syntax. If no response format is set, the agent returns its standard outputs: content, model, tokens, and toolCalls." },
|
||||
{ question: "What does the Reasoning Effort / Thinking Level setting do?", answer: "These are advanced settings that appear only for models that support extended reasoning. Reasoning Effort (for OpenAI o-series and GPT-5 models) and Thinking Level (for Anthropic Claude and Gemini models with thinking) control how much compute the model spends reasoning before responding. Higher levels produce more thorough answers but cost more tokens and take longer." },
|
||||
{ question: "How does max output tokens work with Anthropic models specifically?", answer: "The Agent block uses each Anthropic model's full max output token limit by default (e.g., 64,000 for Claude Sonnet 4.5). You can override this with the Max Tokens setting. For non-streaming requests that exceed the SDK's internal threshold, the provider automatically uses internal streaming to avoid timeouts." },
|
||||
{ question: "Can I use the Agent block with a custom or self-hosted model?", answer: "Yes. You can use any Ollama or VLLM-compatible model by typing the model name directly into the model combobox. This lets you connect to locally hosted or custom-deployed models as long as they expose a compatible API endpoint." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: API
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The API block connects your workflow to external services through HTTP requests. Supports GET, POST, PUT, DELETE, and PATCH methods for interacting with REST APIs.
|
||||
|
||||
@@ -79,7 +80,6 @@ After an API request completes, you can access its outputs:
|
||||
- **`<api.data>`**: The response body data from the API
|
||||
- **`<api.status>`**: HTTP status code (200, 404, 500, etc.)
|
||||
- **`<api.headers>`**: Response headers from the server
|
||||
- **`<api.error>`**: Error details if the request failed
|
||||
|
||||
## Advanced Features
|
||||
|
||||
@@ -127,7 +127,6 @@ if (<api.status> === 200) {
|
||||
- **`<api.data>`**: Response body data from the API
|
||||
- **`<api.status>`**: HTTP status code
|
||||
- **`<api.headers>`**: Response headers
|
||||
- **`<api.error>`**: Error details if request failed
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
@@ -147,3 +146,12 @@ Function (Validate) → API (Stripe) → Condition (Success) → Supabase (Updat
|
||||
- **Handle errors gracefully**: Connect error handling logic for failed requests
|
||||
- **Validate responses**: Check status codes and response formats before processing data
|
||||
- **Respect rate limits**: Be mindful of API rate limits and implement appropriate throttling
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What is the default request timeout?", answer: "The default timeout is 300,000 milliseconds (5 minutes). You can configure it up to a maximum of 600,000 milliseconds (10 minutes) in the block's Advanced settings." },
|
||||
{ question: "Which HTTP errors trigger automatic retries?", answer: "Retries are attempted for network/connection failures, timeouts, rate limit responses (HTTP 429), and server errors (5xx). Client errors like 400 or 404 are not retried." },
|
||||
{ question: "How does retry backoff work?", answer: "Retries use exponential backoff starting from the configured retry delay (default 500ms). Each subsequent retry doubles the delay, up to the maximum retry delay (default 30,000ms)." },
|
||||
{ question: "Are POST and PATCH requests retried by default?", answer: "No. POST and PATCH are non-idempotent methods, so retries are disabled for them by default to avoid creating duplicate resources. You can enable retries for these methods with the 'Retry non-idempotent methods' toggle in Advanced settings, but be aware this may cause duplicate requests." },
|
||||
{ question: "What headers are included automatically?", answer: "Standard headers such as User-Agent, Accept, and Cache-Control are added automatically. Any custom headers you configure will be merged with these defaults, and your custom values will override automatic headers with the same name." },
|
||||
{ question: "Can I send form data or file uploads?", answer: "The API block primarily sends JSON request bodies through the UI. The underlying HTTP tool also supports form data natively — if you pass form data parameters, it will construct a proper multipart/form-data request automatically. For most use cases, the JSON body field in the block UI is sufficient." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Condition
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Condition block branches workflow execution based on boolean expressions. Evaluate conditions using previous block outputs and route to different paths without requiring an LLM.
|
||||
|
||||
@@ -60,10 +61,9 @@ Conditions use JavaScript syntax and can reference input values from previous bl
|
||||
|
||||
After a condition evaluates, you can access its outputs:
|
||||
|
||||
- **`<condition.result>`**: Boolean result of the condition evaluation
|
||||
- **`<condition.matched_condition>`**: ID of the condition that was matched
|
||||
- **`<condition.content>`**: Description of the evaluation result
|
||||
- **`<condition.path>`**: Details of the chosen routing destination
|
||||
- **`<condition.conditionResult>`**: Boolean result of the condition evaluation
|
||||
- **`<condition.selectedOption>`**: ID of the condition that was matched
|
||||
- **`<condition.selectedPath>`**: Details of the chosen routing destination
|
||||
|
||||
## Advanced Features
|
||||
|
||||
@@ -102,18 +102,13 @@ true
|
||||
|
||||
### Error Handling
|
||||
|
||||
Conditions automatically handle:
|
||||
- Undefined or null values with safe evaluation
|
||||
- Type mismatches with appropriate fallbacks
|
||||
- Invalid expressions with error logging
|
||||
- Missing variables with default values
|
||||
If a condition expression references an undefined variable or throws a runtime error, the block will throw an error and the execution will fail (or follow the error path if one is connected). Use optional chaining (`?.`) or explicit null checks in your expressions to handle missing values safely.
|
||||
|
||||
## Outputs
|
||||
|
||||
- **`<condition.result>`**: Boolean result of the evaluation
|
||||
- **`<condition.matched_condition>`**: ID of the matched condition
|
||||
- **`<condition.content>`**: Description of the evaluation result
|
||||
- **`<condition.path>`**: Details of the chosen routing destination
|
||||
- **`<condition.conditionResult>`**: Boolean result of the evaluation
|
||||
- **`<condition.selectedOption>`**: ID of the matched condition
|
||||
- **`<condition.selectedPath>`**: Details of the chosen routing destination
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
@@ -139,3 +134,11 @@ Function (Process) → Condition (account_type === 'enterprise') → Advanced or
|
||||
- **Keep expressions simple**: Use clear, straightforward boolean expressions for better readability and easier debugging
|
||||
- **Document your conditions**: Add descriptions to explain the purpose of each condition for better team collaboration and maintenance
|
||||
- **Test edge cases**: Verify conditions handle boundary values correctly by testing with values at the edges of your condition ranges
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Does the Condition block use an LLM?", answer: "No. The Condition block evaluates boolean expressions using JavaScript syntax directly — it does not call any AI model. This makes it fast, deterministic, and free of API costs. If you need AI-powered routing decisions, use the Router block instead." },
|
||||
{ question: "What happens if no condition matches?", answer: "If none of your defined conditions evaluate to true, the workflow follows the else branch. If the else branch is not connected to any downstream block, that workflow path ends gracefully without an error. Add a fallback condition of simply true as the last condition to guarantee a match." },
|
||||
{ question: "In what order are conditions evaluated?", answer: "Conditions are evaluated from top to bottom in the order they are defined. The first condition that evaluates to true determines the execution path. Subsequent conditions are not evaluated after a match is found, so place more specific conditions before general ones." },
|
||||
{ question: "What JavaScript features can I use in condition expressions?", answer: "You can use standard JavaScript operators and methods including comparison operators (===, !==, >, <), logical operators (&&, ||, !), string methods (.includes(), .endsWith(), .toLowerCase()), array methods (.includes(), .length), mathematical operations, and Date comparisons. Reference block outputs using <blockName.output> syntax." },
|
||||
{ question: "How does the Condition block handle null or undefined values?", answer: "If a condition expression references an undefined variable or throws a runtime error, the Condition block will throw an error and the execution will fail (or follow the error path if one is connected). Use optional chaining (?.) or explicit null checks in your expressions to handle missing values safely." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Evaluator
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Evaluator block uses AI to score and assess content quality against custom metrics. Perfect for quality control, A/B testing, and ensuring AI outputs meet specific standards.
|
||||
|
||||
@@ -49,12 +50,12 @@ The content to be evaluated. This can be:
|
||||
Choose an AI model to perform the evaluation:
|
||||
|
||||
- **OpenAI**: GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
- **Anthropic**: Claude 3.7 Sonnet
|
||||
- **Anthropic**: Claude Sonnet 4.5
|
||||
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
|
||||
- **Other Providers**: Groq, Cerebras, xAI, DeepSeek
|
||||
- **Local Models**: Ollama or VLLM compatible models
|
||||
|
||||
Use models with strong reasoning capabilities like GPT-4o or Claude 3.7 Sonnet for best results.
|
||||
Use models with strong reasoning capabilities like GPT-4o or Claude Sonnet 4.5 for best results.
|
||||
|
||||
### API Key
|
||||
|
||||
@@ -91,3 +92,12 @@ Agent (Support Response) → Evaluator (Score) → Function (Log) → Condition
|
||||
- **Connect with Agent blocks**: Use Evaluator blocks to assess Agent block outputs and create feedback loops
|
||||
- **Use consistent metrics**: For comparative analysis, maintain consistent metrics across similar evaluations
|
||||
- **Combine multiple metrics**: Use several metrics to get a comprehensive evaluation
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What format does the Evaluator return scores in?", answer: "The Evaluator returns a JSON object where each key is the lowercase version of your metric name and the value is a numeric score within the range you defined. For example, a metric named 'Accuracy' with range 1-5 would appear as { \"accuracy\": 4 } in the output." },
|
||||
{ question: "Which models work best for evaluation?", answer: "Models with strong reasoning capabilities produce the most consistent evaluations. GPT-4o and Claude Sonnet are recommended. The default model is Claude Sonnet 4.5." },
|
||||
{ question: "Can I evaluate non-text content?", answer: "The content field accepts any string input. If you pass JSON or structured data, the Evaluator will automatically detect and format it before evaluation. However, the evaluation is text-based — it cannot directly evaluate images or audio." },
|
||||
{ question: "What happens if a metric name is invalid or incomplete?", answer: "Metrics missing a name or range are automatically filtered out. The Evaluator only scores metrics that have both a valid name and a defined min/max range." },
|
||||
{ question: "Does the Evaluator use structured output?", answer: "Yes. The Evaluator generates a JSON Schema response format based on your metrics and enforces strict mode, so the LLM is constrained to return only the expected metric scores as numbers — no extra text or explanations." },
|
||||
{ question: "How are evaluation costs calculated?", answer: "Costs are based on the token usage of the underlying LLM call. The Evaluator outputs include token counts (prompt, completion, total) and cost breakdown (input, output, total) so you can track spending per evaluation." },
|
||||
]} />
|
||||
|
||||
@@ -3,6 +3,7 @@ title: Function
|
||||
---
|
||||
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Function block executes custom JavaScript or TypeScript code in your workflows. Transform data, perform calculations, or implement custom logic.
|
||||
|
||||
@@ -71,3 +72,12 @@ return {
|
||||
- **Test edge cases**: Ensure your code handles unusual inputs, null values, and boundary conditions correctly
|
||||
- **Optimize for performance**: Be mindful of computational complexity and memory usage for large datasets
|
||||
- **Use console.log() for debugging**: Leverage stdout output to debug and monitor function execution
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What languages does the Function block support?", answer: "The Function block supports JavaScript and Python. JavaScript is the default. Python support requires the E2B feature to be enabled, as Python code always runs in a secure E2B sandbox environment." },
|
||||
{ question: "When does code run locally vs. in a sandbox?", answer: "JavaScript code without external imports runs in a local isolated VM for fast execution. JavaScript code that uses import or require statements requires E2B and runs in a secure sandbox. Python code always runs in the E2B sandbox regardless of whether it has imports." },
|
||||
{ question: "How do I reference outputs from other blocks inside my code?", answer: "Use the angle bracket syntax directly in your code, like <agent.content> or <api.data>. Do not wrap these references in quotes — the system replaces them with actual values before execution. For environment variables, use double curly braces: {{API_KEY}}." },
|
||||
{ question: "What does the Function block return?", answer: "The Function block has two outputs: result (the return value of your code, accessed via <function.result>) and stdout (anything logged with console.log(), accessed via <function.stdout>). Make sure your code includes a return statement if you need to pass data to downstream blocks." },
|
||||
{ question: "Can I make HTTP requests from a Function block?", answer: "Yes. The fetch() API is available in the JavaScript execution environment. You can use async/await with fetch to call external APIs. However, you cannot use libraries like axios or request — only the built-in fetch is supported. Your code runs inside an async context automatically, so you can use await directly." },
|
||||
{ question: "Is there a timeout for Function block execution?", answer: "Yes. Function blocks have a configurable execution timeout. If your code exceeds the timeout, the execution is terminated and the block reports an error. Keep this in mind when making external API calls or processing large datasets." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Guardrails
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Guardrails block validates and protects your AI workflows by checking content against multiple validation types. Ensure data quality, prevent hallucinations, detect PII, and enforce format requirements before content moves through your workflow.
|
||||
|
||||
@@ -66,8 +67,8 @@ Uses Retrieval-Augmented Generation (RAG) with LLM scoring to detect when AI-gen
|
||||
- **Knowledge Base**: Select from your existing knowledge bases
|
||||
- **Model**: Choose LLM for scoring (requires strong reasoning - GPT-4o, Claude 3.7 Sonnet recommended)
|
||||
- **API Key**: Authentication for selected LLM provider (auto-hidden for hosted/Ollama or VLLM compatible models)
|
||||
- **Confidence Threshold**: Minimum score to pass (0-10, default: 3)
|
||||
- **Top K** (Advanced): Number of knowledge base chunks to retrieve (default: 10)
|
||||
- **Confidence**: Minimum score to pass (0-10, default: 3)
|
||||
- **Top K** (Advanced): Number of knowledge base chunks to retrieve (default: 5)
|
||||
|
||||
**Output:**
|
||||
- `passed`: `true` if confidence score ≥ threshold
|
||||
@@ -83,7 +84,7 @@ Uses Retrieval-Augmented Generation (RAG) with LLM scoring to detect when AI-gen
|
||||
|
||||
### PII Detection
|
||||
|
||||
Detects personally identifiable information using Microsoft Presidio. Supports 40+ entity types across multiple countries and languages.
|
||||
Detects personally identifiable information using Microsoft Presidio. Supports over 30 entity types across multiple countries and languages.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
@@ -98,7 +99,7 @@ Detects personally identifiable information using Microsoft Presidio. Supports 4
|
||||
**How It Works:**
|
||||
1. Pass content to validate (e.g., `<agent1.content>`)
|
||||
2. Select PII types to detect using the modal selector
|
||||
3. Choose detection mode (Detect or Mask)
|
||||
3. Choose the action (Block Request or Mask PII)
|
||||
4. Content is scanned for matching PII entities
|
||||
5. Returns detection results and optionally masked text
|
||||
|
||||
@@ -109,17 +110,17 @@ Detects personally identifiable information using Microsoft Presidio. Supports 4
|
||||
**Configuration:**
|
||||
- **PII Types to Detect**: Select from grouped categories via modal selector
|
||||
- **Common**: Person name, Email, Phone, Credit card, IP address, etc.
|
||||
- **USA**: SSN, Driver's license, Passport, etc.
|
||||
- **USA**: SSN, Driver's license, Passport, Bank account number, ITIN
|
||||
- **UK**: NHS number, National insurance number
|
||||
- **Spain**: NIF, NIE, CIF
|
||||
- **Italy**: Fiscal code, Driver's license, VAT code
|
||||
- **Poland**: PESEL, NIP, REGON
|
||||
- **Singapore**: NRIC/FIN, UEN
|
||||
- **Spain**: NIF, NIE
|
||||
- **Italy**: Fiscal code, Driver's license, Identity card, Passport
|
||||
- **Poland**: PESEL
|
||||
- **Singapore**: NRIC/FIN
|
||||
- **Australia**: ABN, ACN, TFN, Medicare
|
||||
- **India**: Aadhaar, PAN, Passport, Voter number
|
||||
- **Mode**:
|
||||
- **Detect**: Only identify PII (default)
|
||||
- **Mask**: Replace detected PII with masked values
|
||||
- **India**: Aadhaar, PAN, Vehicle registration, Voter number, Passport
|
||||
- **Action**:
|
||||
- **Block Request**: Only identify PII (default)
|
||||
- **Mask PII**: Replace detected PII with masked values
|
||||
- **Language**: Detection language (default: English)
|
||||
|
||||
**Output:**
|
||||
@@ -140,7 +141,7 @@ Detects personally identifiable information using Microsoft Presidio. Supports 4
|
||||
|
||||
The input content to validate. This typically comes from:
|
||||
- Agent block outputs: `<agent.content>`
|
||||
- Function block results: `<function.output>`
|
||||
- Function block results: `<function.result>`
|
||||
- API responses: `<api.output>`
|
||||
- Any other block output
|
||||
|
||||
@@ -203,3 +204,13 @@ Input → Guardrails (Detect PII) → Condition (No PII) → Process or Reject
|
||||
Guardrails validation happens synchronously in your workflow. For hallucination detection, choose faster models (like GPT-4o-mini) if latency is critical.
|
||||
</Callout>
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Can I run multiple validation types on the same content?", answer: "Each Guardrails block performs one validation type at a time. To apply multiple validations, chain several Guardrails blocks in sequence — for example, first validate JSON format, then check for PII." },
|
||||
{ question: "What does the hallucination confidence score mean?", answer: "The score ranges from 0 to 10. A score of 0 means the content is completely ungrounded (full hallucination), and 10 means it is fully supported by the knowledge base. Validation passes when the score meets or exceeds your configured threshold (default: 3)." },
|
||||
{ question: "How many knowledge base chunks are retrieved for hallucination detection?", answer: "By default, 5 chunks are retrieved. You can adjust this up to 20 in the Advanced settings using the 'Number of Chunks to Retrieve' slider. More chunks provide broader context but increase latency and token usage." },
|
||||
{ question: "What PII detection engine is used?", answer: "PII detection is powered by Microsoft Presidio. It supports over 30 entity types across multiple countries including the US, UK, Spain, Italy, Poland, Singapore, Australia, and India." },
|
||||
{ question: "What is the difference between Block and Mask modes for PII?", answer: "Block mode fails the validation (passed = false) if any selected PII types are detected. Mask mode also detects PII but replaces it with masked values in the output, making the content safe to use downstream. Both modes return the list of detected entities." },
|
||||
{ question: "Which languages does PII detection support?", answer: "PII detection supports English, Spanish, Italian, Polish, and Finnish. The language setting affects the NLP models used for entity recognition, so selecting the correct language improves detection accuracy." },
|
||||
{ question: "Does JSON validation check schema structure or just syntax?", answer: "JSON validation only checks that the content is syntactically valid JSON (i.e., it can be parsed without errors). It does not validate against a specific schema. For schema validation, use a Function block after the Guardrails check." },
|
||||
]} />
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Human in the Loop block pauses workflow execution and waits for human intervention before continuing. Use it to add approval gates, collect feedback, or gather additional input at critical decision points.
|
||||
|
||||
@@ -33,7 +34,7 @@ When execution reaches this block, the workflow pauses indefinitely until a huma
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Paused Output
|
||||
### Display Data
|
||||
|
||||
Defines what data is displayed to the approver. This is the context shown in the approval portal to help them make an informed decision.
|
||||
|
||||
@@ -60,7 +61,7 @@ Configures how approvers are alerted when approval is needed. Supported channels
|
||||
|
||||
Include the approval URL (`<blockId.url>`) in your notification messages so approvers can access the portal.
|
||||
|
||||
### Resume Input
|
||||
### Resume Form
|
||||
|
||||
Defines the fields approvers fill in when responding. This data becomes available to downstream blocks after the workflow resumes.
|
||||
|
||||
@@ -136,8 +137,12 @@ Agent (Generate) → Human in the Loop (QA) → Gmail (Send)
|
||||
|
||||
## Block Outputs
|
||||
|
||||
**`url`** - Unique URL for the approval portal
|
||||
**`resumeInput.*`** - All fields defined in Resume Input become available after the workflow resumes
|
||||
**`url`** - Unique URL for the approval portal
|
||||
**`resumeEndpoint`** - Resume API endpoint URL
|
||||
**`response`** - Display data shown to the approver (json)
|
||||
**`submission`** - Form submission data from the approver (json)
|
||||
**`submittedAt`** - ISO timestamp when the workflow was resumed
|
||||
**`resumeInput.*`** - All fields defined in Resume Form become available after the workflow resumes
|
||||
|
||||
Access using `<blockId.resumeInput.fieldName>`.
|
||||
|
||||
@@ -176,3 +181,12 @@ The example below shows an approval portal as seen by an approver after the work
|
||||
- **[Condition](/blocks/condition)** - Branch based on approval decisions
|
||||
- **[Variables](/blocks/variables)** - Store approval history and metadata
|
||||
- **[Response](/blocks/response)** - Return workflow results to API callers
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How long does the workflow stay paused?", answer: "The workflow pauses indefinitely until a human provides input through the approval portal, the REST API, or a webhook. There is no automatic timeout — it will wait until someone responds." },
|
||||
{ question: "What notification channels can I use to alert approvers?", answer: "You can configure notifications through Slack, Gmail, Microsoft Teams, SMS (via Twilio), or custom webhooks. Include the approval URL in your notification message so approvers can access the portal directly." },
|
||||
{ question: "How do I access the approver's input in downstream blocks?", answer: "Use the syntax <blockId.resumeInput.fieldName> to reference specific fields from the resume form. For example, if your block ID is 'approval1' and the form has an 'approved' field, use <approval1.resumeInput.approved>." },
|
||||
{ question: "Can I chain multiple Human in the Loop blocks for multi-stage approvals?", answer: "Yes. You can place multiple Human in the Loop blocks in sequence to create multi-stage approval workflows. Each block pauses independently and can have its own notification configuration and resume form fields." },
|
||||
{ question: "Can I resume the workflow programmatically without the portal?", answer: "Yes. Each block exposes a resume API endpoint that you can call with a POST request containing the form data as JSON. This lets you build custom approval UIs or integrate with existing systems like Jira or ServiceNow." },
|
||||
{ question: "What outputs are available after the workflow resumes?", answer: "The block outputs include the approval portal URL, the resume API endpoint URL, the display data shown to the approver, the form submission data, the raw resume input, and an ISO timestamp of when the workflow was resumed." },
|
||||
]} />
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Blocks are the building components you connect together to create AI workflows. Think of them as specialized modules that each handle a specific task—from chatting with AI models to making API calls or processing data.
|
||||
|
||||
@@ -138,3 +139,12 @@ Each block type has specific configuration options:
|
||||
Pause workflow execution for specified time delays
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How many block types are available in Sim?", answer: "Sim has over 200 blocks in its registry, spanning core workflow blocks (Agent, Function, Condition, Router, etc.), integration blocks for third-party services (Gmail, Slack, GitHub, Notion, and many more), and trigger blocks that start workflows from external events like webhooks or schedules. Loop and parallel execution are built into the execution engine as container constructs on the canvas, rather than being standalone registry blocks." },
|
||||
{ question: "Can one block's output connect to multiple downstream blocks?", answer: "Yes. A single output port can connect to multiple input ports on different blocks. This lets you fan out data from one processing step to several parallel paths without needing to duplicate the block." },
|
||||
{ question: "What happens if a block in the middle of a workflow fails?", answer: "When a block encounters an error, the workflow stops executing along that path. Blocks that support error handling (like the Router) can route to an error path so you can handle failures gracefully instead of halting the entire workflow." },
|
||||
{ question: "What is the difference between Processing blocks and Logic blocks?", answer: "Processing blocks (Agent, Function, API) transform or generate data — they do the actual work. Logic blocks (Condition, Router, Evaluator) make decisions about which path the workflow should take based on the data, without modifying it themselves." },
|
||||
{ question: "Can I use blocks from different categories together in one workflow?", answer: "Absolutely. A typical workflow combines blocks from multiple categories. For example, you might use a trigger block to start the workflow, an Agent block to process input, a Condition block to branch logic, and an integration block like Gmail to send results." },
|
||||
{ question: "Are there container blocks that can hold other blocks inside them?", answer: "Yes. Loop and Parallel are execution engine constructs that appear as container regions on the canvas. You drag other blocks inside them. Loop containers execute their contained blocks repeatedly, while Parallel containers execute their contained blocks concurrently across multiple branches. Unlike registry blocks, these are handled directly by the execution engine." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Loop
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Loop block is a container that executes blocks repeatedly. Iterate over collections, repeat operations a fixed number of times, or continue while a condition is met.
|
||||
|
||||
@@ -184,13 +185,8 @@ Variables (i=0) → Loop (While i<10) → Agent (Process) → Variables (i++)
|
||||
|
||||
### Limitations
|
||||
|
||||
<Callout type="warning">
|
||||
Container blocks (Loops and Parallels) cannot be nested inside each other. This means:
|
||||
- You cannot place a Loop block inside another Loop block
|
||||
- You cannot place a Parallel block inside a Loop block
|
||||
- You cannot place any container block inside another container block
|
||||
|
||||
If you need multi-dimensional iteration, consider restructuring your workflow to use sequential loops or process data in stages.
|
||||
<Callout type="info">
|
||||
Container blocks (Loops and Parallels) support nesting. You can place loops inside loops, parallels inside loops, and any combination of container blocks to build complex multi-dimensional workflows.
|
||||
</Callout>
|
||||
|
||||
<Callout type="info">
|
||||
@@ -250,3 +246,13 @@ Variables (i=0) → Loop (While i<10) → Agent (Process) → Variables (i++)
|
||||
- **Set reasonable limits**: Keep iteration counts reasonable to avoid long execution times
|
||||
- **Use ForEach for collections**: When processing arrays or objects, use ForEach instead of For loops
|
||||
- **Handle errors gracefully**: Consider adding error handling inside loops for robust workflows
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What is the maximum number of iterations a loop can run?", answer: "For loops (fixed count) and ForEach loops are capped at 1,000 iterations/items (from executor constants). While loops and Do-While loops with a condition have no hard iteration cap — they run until the condition evaluates to false. Do-While loops without a condition fall back to a fixed iteration count, which is capped at 1,000. Always ensure your While/Do-While conditions will eventually become false to avoid infinite loops." },
|
||||
{ question: "Do loops execute iterations in parallel or sequentially?", answer: "Loops execute all iterations sequentially, one after another. If you need concurrent execution across items, use the Parallel block instead. You can also nest a Parallel block inside a Loop if you need both iteration patterns." },
|
||||
{ question: "How do I access the current item inside a ForEach loop?", answer: "Inside the loop, use <loop.currentItem> to get the current item being processed and <loop.index> for the zero-based iteration number. These references are only available to blocks placed inside the loop container — blocks outside the loop cannot access them." },
|
||||
{ question: "How do I access loop results after it finishes?", answer: "After the loop completes, reference the loop block by its normalized name (lowercase, no spaces) using <blockname.results>. This returns an array of all iteration results in order. For example, if your loop block is named 'Process Items', use <processitems.results>. Do not use <loop.> syntax outside the loop — that only works inside." },
|
||||
{ question: "Can I nest loops inside each other?", answer: "Yes. Container blocks (Loops and Parallels) fully support nesting. You can place loops inside loops, parallels inside loops, loops inside parallels, and any combination. Each nested container maintains its own scope and iteration context independently." },
|
||||
{ question: "What is the difference between a While loop and a Do-While loop?", answer: "A While loop checks its condition before each iteration, so it may execute zero times if the condition is false initially. A Do-While loop executes its body at least once, then checks the condition after each iteration to decide whether to continue. Use Do-While when you need guaranteed first execution." },
|
||||
{ question: "What happens if a ForEach loop receives an empty collection?", answer: "If the ForEach loop's collection is empty, the loop body is skipped entirely and the loop outputs an empty results array. The workflow continues normally to any blocks connected after the loop." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Parallel
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Parallel block is a container that executes multiple instances concurrently for faster workflow processing. Process items simultaneously instead of sequentially.
|
||||
|
||||
@@ -148,11 +149,8 @@ Each parallel instance runs independently:
|
||||
|
||||
### Limitations
|
||||
|
||||
<Callout type="warning">
|
||||
Container blocks (Loops and Parallels) cannot be nested inside each other. This means:
|
||||
- You cannot place a Loop block inside a Parallel block
|
||||
- You cannot place another Parallel block inside a Parallel block
|
||||
- You cannot place any container block inside another container block
|
||||
<Callout type="info">
|
||||
Container blocks (Loops and Parallels) support nesting. You can place parallels inside parallels, loops inside parallels, and any combination of container blocks to build complex multi-dimensional workflows.
|
||||
</Callout>
|
||||
|
||||
<Callout type="info">
|
||||
@@ -221,3 +219,13 @@ Understanding when to use each:
|
||||
- **Independent operations only**: Ensure operations don't depend on each other
|
||||
- **Handle rate limits**: Add delays or throttling for API-heavy workflows
|
||||
- **Error handling**: Each instance should handle its own errors gracefully
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What is the maximum number of concurrent instances?", answer: "The maximum is 20 concurrent instances. This limit exists to prevent resource exhaustion and ensure stable execution." },
|
||||
{ question: "Can parallel instances share state with each other?", answer: "No. Each parallel instance runs in complete isolation with its own variable scope. There is no shared state between instances, and one instance cannot read or write data from another during execution." },
|
||||
{ question: "What happens if one parallel instance fails?", answer: "Failures in one instance do not affect other instances. Each instance runs independently, so the remaining instances will continue to execute normally." },
|
||||
{ question: "Can I nest Parallel blocks inside other Parallel or Loop blocks?", answer: "Yes. Container blocks (Parallels and Loops) support nesting. You can place parallels inside parallels, loops inside parallels, and any combination to build multi-dimensional workflows." },
|
||||
{ question: "How do I access results after the Parallel block completes?", answer: "Use <blockname.results> where blockname is the normalized name of your Parallel block (lowercase, no spaces). This returns an array containing the results from all instances." },
|
||||
{ question: "Is the order of results guaranteed?", answer: "No. Because instances execute concurrently, the order of results in the output array is not guaranteed to match the input order. If ordering matters, include an index or identifier in each instance's output." },
|
||||
{ question: "What is the difference between count-based and collection-based parallel?", answer: "Count-based runs a fixed number of identical instances (e.g., run 5 times). Collection-based distributes items from an array or object across instances, with each instance processing one item. Use count-based for repeated operations and collection-based for batch processing." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Response
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Response block formats and sends structured HTTP responses back to API callers. Use it to return workflow results with proper status codes and headers.
|
||||
|
||||
@@ -35,23 +36,15 @@ The response data is the main content that will be sent back to the API caller.
|
||||
|
||||
### Status Code
|
||||
|
||||
Set the HTTP status code for the response (defaults to 200):
|
||||
A free-text input field where you can enter any valid HTTP status code (the default placeholder is 200). Common examples include:
|
||||
|
||||
**Success (2xx):**
|
||||
- **200**: OK - Standard success response
|
||||
- **201**: Created - Resource successfully created
|
||||
- **204**: No Content - Success with no response body
|
||||
|
||||
**Client Error (4xx):**
|
||||
- **400**: Bad Request - Invalid request parameters
|
||||
- **401**: Unauthorized - Authentication required
|
||||
- **404**: Not Found - Resource doesn't exist
|
||||
- **422**: Unprocessable Entity - Validation errors
|
||||
|
||||
**Server Error (5xx):**
|
||||
- **500**: Internal Server Error - Server-side error
|
||||
- **502**: Bad Gateway - External service error
|
||||
- **503**: Service Unavailable - Service temporarily down
|
||||
|
||||
Any valid HTTP status code can be entered directly into the field.
|
||||
|
||||
### Response Headers
|
||||
|
||||
@@ -84,7 +77,7 @@ Condition (Error Detected) → Router → Response (400/500, Error Details)
|
||||
|
||||
## Outputs
|
||||
|
||||
Response blocks are terminal - they end workflow execution and send the HTTP response to the API caller. No outputs are available to downstream blocks.
|
||||
Response blocks are terminal — no downstream blocks execute after them. However, the block does define outputs (`data`, `status`, `headers`) which are used to construct the HTTP response sent back to the API caller.
|
||||
|
||||
## Variable References
|
||||
|
||||
@@ -116,3 +109,11 @@ Use the `<variable.name>` syntax to dynamically insert workflow variables into y
|
||||
- **Handle errors gracefully**: Use conditional logic in your workflow to set appropriate error responses with descriptive messages
|
||||
- **Validate variable references**: Ensure all referenced variables exist and contain the expected data types before the Response block executes
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Can I have multiple Response blocks in a workflow?", answer: "No. The Response block is a single-instance block — only one is allowed per workflow. If you need different responses for different conditions, use a Condition or Router block upstream to determine what data reaches the single Response block." },
|
||||
{ question: "What triggers require a Response block?", answer: "The Response block is designed for use with the API Trigger. When your workflow is invoked via the API, the Response block sends the structured HTTP response back to the caller. Other trigger types (like webhooks or schedules) do not require a Response block." },
|
||||
{ question: "What is the difference between Builder and Editor mode?", answer: "Builder mode provides a visual interface for constructing your response structure with fields and types. Editor mode gives you a raw JSON code editor where you can write the response body directly. Builder mode is recommended for most use cases." },
|
||||
{ question: "What is the default status code?", answer: "If you do not specify a status code, the Response block defaults to 200 (OK). You can set any valid HTTP status code including error codes like 400, 404, or 500." },
|
||||
{ question: "Can the Response block connect to downstream blocks?", answer: "No. Response blocks are terminal — they end workflow execution and send the HTTP response. No further blocks can be connected after a Response block." },
|
||||
]} />
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ title: Router
|
||||
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Router block uses AI to intelligently route workflows based on content analysis. Unlike Condition blocks that use simple rules, Routers understand context and intent. Each route you define creates a separate output port, allowing you to connect different paths to different downstream blocks.
|
||||
|
||||
@@ -54,12 +55,12 @@ Each route you add creates a **separate output port** on the Router block. Conne
|
||||
Choose an AI model to power the routing decision:
|
||||
|
||||
- **OpenAI**: GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
- **Anthropic**: Claude 3.7 Sonnet
|
||||
- **Anthropic**: Claude Sonnet 4.5
|
||||
- **Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
|
||||
- **Other Providers**: Groq, Cerebras, xAI, DeepSeek
|
||||
- **Local Models**: Ollama or VLLM compatible models
|
||||
|
||||
Use models with strong reasoning capabilities like GPT-4o or Claude 3.7 Sonnet for best results.
|
||||
Use models with strong reasoning capabilities like GPT-4o or Claude Sonnet 4.5 for best results.
|
||||
|
||||
### API Key
|
||||
|
||||
@@ -68,11 +69,12 @@ Your API key for the selected LLM provider. This is securely stored and used for
|
||||
## Outputs
|
||||
|
||||
- **`<router.context>`**: The context that was analyzed
|
||||
- **`<router.selectedRoute>`**: The ID of the selected route
|
||||
- **`<router.selected_path>`**: Details of the chosen destination block
|
||||
- **`<router.model>`**: Model used for decision-making
|
||||
- **`<router.tokens>`**: Token usage statistics
|
||||
- **`<router.cost>`**: Estimated routing cost
|
||||
- **`<router.model>`**: Model used for decision-making
|
||||
- **`<router.selectedRoute>`**: The ID of the selected route
|
||||
- **`<router.reasoning>`**: Explanation of why this route was chosen
|
||||
- **`<router.selectedPath>`**: Details of the chosen destination block
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
@@ -117,3 +119,12 @@ When the Router cannot determine an appropriate route for the given context, it
|
||||
- **Test with diverse inputs**: Ensure the Router handles various input types, edge cases, and unexpected content.
|
||||
- **Monitor routing performance**: Review routing decisions regularly and refine route descriptions based on actual usage patterns.
|
||||
- **Choose appropriate models**: Use models with strong reasoning capabilities for complex routing decisions.
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How does the Router decide which route to take?", answer: "The Router sends your context and all route descriptions to an LLM, which analyzes the input and selects the route whose description best matches. The LLM is prompted to be deterministic: it always prefers selecting a route over returning no match, and only reports NO_MATCH if the context is completely unrelated to every route description." },
|
||||
{ question: "What happens when the Router cannot match any route?", answer: "When the LLM determines that the context does not match any defined route, it returns NO_MATCH and the Router directs execution to the error path. Connect an error handler to this path for graceful fallback behavior rather than letting the workflow fail silently." },
|
||||
{ question: "Does the Router cost money to run?", answer: "Yes. The Router uses an LLM API call for every routing decision, which consumes tokens and incurs costs. You can monitor this via the <router.tokens> and <router.cost> outputs. If your routing logic can be expressed as simple boolean conditions, use the Condition block instead — it is free and faster." },
|
||||
{ question: "Can I see why the Router chose a particular route?", answer: "Yes. The Router V2 block outputs a reasoning field (<router.reasoning>) that contains a brief 1-2 sentence explanation of why the selected route was chosen. This is useful for debugging and understanding routing decisions." },
|
||||
{ question: "What models work best for routing?", answer: "Models with strong reasoning capabilities like GPT-4o or Claude Sonnet 4.5 tend to produce the most accurate routing decisions. For simpler routing scenarios with clearly distinct routes, a faster and cheaper model like GPT-4o-mini or Gemini Flash may be sufficient." },
|
||||
{ question: "How many routes can I define?", answer: "There is no hard limit on the number of routes. Each route you define creates a separate output port on the block. However, keep in mind that more routes with overlapping descriptions make it harder for the LLM to distinguish between them, so aim for clear, mutually exclusive route descriptions." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Variables
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Variables block updates workflow variables during execution. Variables must first be initialized in your workflow's Variables section, then you can use this block to update their values as your workflow runs.
|
||||
|
||||
@@ -73,7 +74,7 @@ API (Fetch Profile) → Variables (userId, userTier) → Agent (Personalize)
|
||||
|
||||
## Outputs
|
||||
|
||||
- **`<variables.assignments>`**: JSON object with all variable assignments from this block
|
||||
The Variables block does not produce traditional block outputs. Variables are accessed globally via `<variable.variableName>` syntax from any block in the workflow, not through block output connections.
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -81,3 +82,12 @@ API (Fetch Profile) → Variables (userId, userTier) → Agent (Personalize)
|
||||
- **Update dynamically**: Use Variables blocks to update values based on block outputs or calculations
|
||||
- **Use in loops**: Perfect for tracking state across iterations
|
||||
- **Name descriptively**: Use clear names like `currentIndex`, `totalProcessed`, or `lastError`
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Do variables persist between workflow executions?", answer: "No. Variables are workflow-scoped and only persist for the duration of a single execution. Each new execution starts with the initial values defined in your workflow's Variables section." },
|
||||
{ question: "Can multiple Variables blocks update the same variable?", answer: "Yes. All Variables blocks share the same namespace. A later Variables block can overwrite a value set by an earlier one by using the same variable name." },
|
||||
{ question: "How do I reference a variable in other blocks?", answer: "Use the <variable.variableName> syntax in any block's input field. For example, <variable.retryCount> or <variable.customerEmail>." },
|
||||
{ question: "Do Variables blocks produce outputs I can wire to other blocks?", answer: "Variables do not appear as traditional block outputs in the connection UI. Instead, they are accessed globally via the <variable.variableName> prefix from any block in the workflow." },
|
||||
{ question: "What naming convention should I use for variables?", answer: "Use descriptive names in camelCase or snake_case. Variable names are case-sensitive, so 'retryCount' and 'RetryCount' are treated as different variables." },
|
||||
{ question: "Can I use block outputs to set variable values?", answer: "Yes. You can reference any block output when setting a variable value, such as setting customerEmail to <api.email> or incrementing a counter based on previous values." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Wait
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Wait block pauses your workflow for a specified amount of time before continuing to the next block. Use it to add delays between actions, respect API rate limits, or space out operations.
|
||||
|
||||
@@ -62,3 +63,11 @@ API (Trigger Job) → Wait (30s) → API (Check Status)
|
||||
|
||||
- **Keep waits reasonable**: Use Wait for delays up to 10 minutes. For longer delays, consider scheduled workflows
|
||||
- **Monitor execution time**: Remember that waits extend total workflow duration
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What is the maximum wait time?", answer: "The maximum wait time is 600 seconds (10 minutes). You can specify the duration in either seconds or minutes." },
|
||||
{ question: "Can a Wait block be cancelled?", answer: "Yes. Wait blocks are interruptible via workflow cancellation. If the workflow is stopped while a Wait block is active, the wait is cancelled and the status output will reflect 'cancelled'." },
|
||||
{ question: "What happens if I enter a value exceeding the maximum?", answer: "The wait is capped at 600 seconds. If you enter a value greater than 600 seconds (or greater than 10 minutes), it will be limited to the maximum allowed duration." },
|
||||
{ question: "Does the Wait block consume resources while paused?", answer: "The Wait block performs a simple sleep for the configured duration. It does not actively consume compute resources during the wait, but the workflow execution remains open until the wait completes." },
|
||||
{ question: "What outputs does the Wait block provide?", answer: "The Wait block outputs the wait duration in milliseconds (waitDuration) and a status string that indicates whether the wait is 'waiting', 'completed', or 'cancelled'." },
|
||||
]} />
|
||||
|
||||
@@ -4,6 +4,7 @@ title: Webhook
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The Webhook block sends HTTP POST requests to external webhook endpoints with automatic webhook headers and optional HMAC signing.
|
||||
|
||||
@@ -85,3 +86,11 @@ Condition (check) → Webhook (trigger) → Response
|
||||
<Callout>
|
||||
The Webhook block always uses POST. For other HTTP methods or more control, use the [API block](/blocks/api).
|
||||
</Callout>
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Can I use HTTP methods other than POST?", answer: "No. The Webhook block always sends POST requests. If you need GET, PUT, DELETE, or PATCH, use the API block instead, which supports all standard HTTP methods." },
|
||||
{ question: "How does HMAC payload signing work?", answer: "When you provide a signing secret, the block generates an HMAC-SHA256 signature of the payload and includes it in the X-Webhook-Signature header in the format t=timestamp,v1=signature. The receiver can verify by computing HMAC-SHA256(secret, \"timestamp.body\") and comparing with the v1 value." },
|
||||
{ question: "What headers are added automatically?", answer: "Every webhook request automatically includes Content-Type (application/json), X-Webhook-Timestamp (Unix timestamp in milliseconds), X-Delivery-ID (unique UUID), and Idempotency-Key (same as X-Delivery-ID for deduplication)." },
|
||||
{ question: "Can custom headers override the automatic ones?", answer: "Yes. Any custom headers you define in the Additional Headers section will override automatic headers that share the same name." },
|
||||
{ question: "How is the Webhook block different from the API block?", answer: "The Webhook block is purpose-built for webhook delivery: it is POST-only, automatically adds webhook-specific headers (timestamp, delivery ID, idempotency key), and supports optional HMAC signing. The API block is more general-purpose with support for all HTTP methods, query parameters, and configurable retries." },
|
||||
]} />
|
||||
|
||||
@@ -39,6 +39,8 @@ Drop a Workflow block when you want to call a child workflow as part of a larger
|
||||
- `result` – the child workflow's final response
|
||||
- `success` – whether it ran without errors
|
||||
- `error` – message when the run fails
|
||||
- `childWorkflowName` – the name of the child workflow (string)
|
||||
- `childWorkflowId` – the ID of the child workflow (string)
|
||||
|
||||
## Deployment Status Badge
|
||||
|
||||
@@ -61,3 +63,14 @@ The Workflow block always executes the most recent deployed version of the child
|
||||
<Callout>
|
||||
Keep child workflows focused. Small, reusable flows make it easier to combine them without creating deep nesting.
|
||||
</Callout>
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Can a workflow call itself recursively?", answer: "No. The workflow selector blocks self-references to prevent infinite loops. Additionally, Sim tracks the call chain across nested executions using an internal header and enforces a maximum call chain depth of 25 hops. If the limit is exceeded, the execution is rejected with a 409 error." },
|
||||
{ question: "What is the maximum nesting depth for sub-workflows?", answer: "The maximum call chain depth is 25. This means workflow A can call B, which calls C, and so on up to 25 levels deep. This limit applies across all chained calls, not just direct parent-child relationships." },
|
||||
{ question: "Does the Workflow block use the deployed or draft version of the child workflow?", answer: "The child workflow inherits the execution context of the parent. If the parent is running in a deployed context (API, schedule, webhook), the child also uses its deployed version. If the parent is running in draft mode (manual run from the editor), the child also uses its draft state. This lets you test nested workflows end-to-end before deploying." },
|
||||
{ question: "How do I pass data to a child workflow?", answer: "Use the Inputs field on the Workflow block. If the child workflow has an Input Form trigger, each field appears in the block configuration and you can map parent variables to them. The mapped values are available as start.input in the child workflow." },
|
||||
{ question: "What outputs does the Workflow block return?", answer: "The block returns a success boolean, the child workflow's result (its final response output), the child workflow's name and ID, and an error message if the run failed. You can reference these outputs from downstream blocks using the tag syntax." },
|
||||
{ question: "What happens if the child workflow fails?", answer: "The Workflow block raises an error that propagates to the parent workflow. If you need to handle failures gracefully, connect an error path from the Workflow block to a downstream block that processes the error." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Basics
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
## How Connections Work
|
||||
|
||||
@@ -47,3 +48,13 @@ The flow of data through connections follows these principles:
|
||||
Deleting a connection will immediately stop data flow between the blocks. Make sure this is
|
||||
intended before removing connections.
|
||||
</Callout>
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How does Sim determine the order blocks execute in?", answer: "Sim builds a directed acyclic graph (DAG) from your connections. Blocks with no unresolved incoming edges execute first. Once a block completes, the engine removes its edge from downstream blocks and queues any block whose incoming edges are all satisfied. This means execution order is entirely determined by how you wire your connections." },
|
||||
{ question: "Can a block have multiple incoming connections?", answer: "Yes. A block with multiple incoming connections will wait until all source blocks have completed before it executes. The engine tracks incoming edges and only marks a block as ready when every incoming edge has been resolved." },
|
||||
{ question: "Can a block send its output to multiple downstream blocks?", answer: "Yes. A single block can have outgoing connections to multiple destination blocks. When the source block completes, all connected downstream blocks that are ready (all their other incoming edges are satisfied) will be queued for execution." },
|
||||
{ question: "What happens to downstream blocks when a Condition or Router block picks a specific path?", answer: "The engine activates only the edge matching the selected condition or route. Edges on unselected paths are deactivated, and any blocks reachable only through those deactivated edges are cascadingly skipped for that execution." },
|
||||
{ question: "Are connections between blocks inside a Loop or Parallel block handled differently?", answer: "Yes. The engine inserts sentinel nodes (start and end) around Loop and Parallel subflows. Connections that cross a loop boundary are redirected through these sentinels, and Loop back-edges are wired automatically so blocks inside the loop re-execute on each iteration." },
|
||||
{ question: "Is there an error-handling path for connections?", answer: "Yes. Connections can use an error source handle. If a block produces an error, only edges marked with the error handle are activated, while the normal source edges are deactivated. This lets you route errors to a dedicated error-handling branch in your workflow." },
|
||||
]} />
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ title: Data Structure
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
When you connect blocks, understanding the data structure of different block outputs is important because the output data structure from the source block determines what values are available in the destination block. Each block type produces a specific output structure that you can reference in downstream blocks.
|
||||
|
||||
@@ -185,3 +186,13 @@ For example:
|
||||
- `<agent1.tokens.total>` - Access the total tokens from an Agent block
|
||||
- `<api1.data.results[0].id>` - Access the ID of the first result from an API response
|
||||
- `<function1.result.calculations.total>` - Access a nested field in a Function block's result
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What output fields does an Agent block produce?", answer: "An Agent block outputs content (the text response), model (the model used, e.g. gpt-4o), tokens (an object with prompt, completion, and total counts), and optionally toolCalls, cost, and usage arrays when tools are invoked." },
|
||||
{ question: "What does the API block output look like?", answer: "The API block returns data (the response body, which can be any type), status (the HTTP status code as a number), and headers (an object containing the response HTTP headers)." },
|
||||
{ question: "What does a Function block return?", answer: "A Function block outputs result (the return value of your function, which can be any type) and stdout (any console output captured during execution)." },
|
||||
{ question: "How does the Condition block output differ from the Router block?", answer: "The Condition block outputs conditionResult (a boolean), selectedPath (with blockId, blockType, and blockTitle of the next block), and selectedOption (the ID of the matched condition). The Router block outputs content (the routing decision text), model, tokens, and selectedPath, but does not include conditionResult or selectedOption." },
|
||||
{ question: "What happens to the Agent block output when a response format schema is configured?", answer: "When you define a response format on an Agent block, the output structure matches your defined schema instead of the standard content/model/tokens structure. Always verify the actual output shape when using response formats." },
|
||||
{ question: "How do I access deeply nested data from an API response?", answer: "Use dot notation with bracket indices in your connection tags. For example, <api1.data.results[0].id> navigates into the data field, then into the results array at index 0, and retrieves the id property." },
|
||||
]} />
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { ConnectIcon } from '@/components/icons'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Connections are the pathways that allow data to flow between blocks in your workflow. They define how information is passed from one block to another, enabling you to create sophisticated, multi-step processes.
|
||||
|
||||
@@ -40,3 +41,12 @@ Sim supports different types of connections that enable various workflow pattern
|
||||
Follow recommended patterns for effective connection management
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How does data actually flow between connected blocks?", answer: "The execution engine builds a directed acyclic graph (DAG) from your connections and processes blocks in dependency order. When a block finishes, its output is stored in the execution context. Downstream blocks reference that output using connection tags like <BlockName.response>, which the variable resolver replaces with the actual data at execution time." },
|
||||
{ question: "Can a block receive input from multiple upstream blocks?", answer: "Yes. A block waits until all of its incoming connections have been fulfilled before it executes. The engine tracks incoming edges for each node and only marks a block as ready when every upstream dependency has completed. You can reference outputs from any connected block using their respective connection tags." },
|
||||
{ question: "What happens if an upstream block fails?", answer: "If a block errors, the engine activates the error edge (if one exists) and deactivates the normal output edge. Downstream blocks on the success path will not execute. You can connect an error handle to a separate block to build fallback or recovery logic." },
|
||||
{ question: "Do connections support conditional branching?", answer: "Yes. Router and Condition blocks produce a selected route or option that determines which outgoing edge is activated. Only the blocks on the chosen path will execute. Edges on unselected paths are deactivated and their entire downstream subgraph is skipped." },
|
||||
{ question: "Can blocks in parallel branches share data with each other?", answer: "Blocks within the same parallel branch cannot directly reference blocks in a sibling branch because branches execute independently. However, once all branches complete and the parallel block exits, downstream blocks can access the aggregated results from all branches." },
|
||||
{ question: "How are connection tags formatted?", answer: "Connection tags use angle-bracket syntax: <BlockName.property>. For nested data you can chain dot notation, such as <BlockName.response.items[0].name>. The resolver walks the object path at execution time and substitutes the resolved value into your input field." },
|
||||
]} />
|
||||
|
||||
@@ -4,6 +4,7 @@ title: Tags
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Connection tags are visual representations of the data available from connected blocks, providing an easy way to reference data between blocks and outputs from previous blocks in your workflow.
|
||||
|
||||
@@ -107,3 +108,14 @@ const total = <apiBlock.data.total> * 1.1; // Add 10% tax
|
||||
When using connection tags in numeric contexts, make sure the referenced data is actually a number
|
||||
to avoid type conversion issues.
|
||||
</Callout>
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How are tag references resolved at runtime?", answer: "The executor uses a chain of resolvers. Each reference like <blockName.path> is matched against resolvers in order: loop references, parallel references, workflow variables, environment variables, and finally block output references. The first resolver that recognizes the reference handles it." },
|
||||
{ question: "Does the block name in a tag reference need to match exactly?", answer: "Block names are normalized by converting to lowercase and removing spaces before matching. So <My Agent.content> and <myagent.content> resolve to the same block. However, the field path after the block name (e.g., content, data.users) is case-sensitive." },
|
||||
{ question: "Can I reference environment variables in tag syntax?", answer: "Yes, but environment variables use double-brace syntax instead of angle brackets: {{MY_VAR}}. These are resolved by a dedicated environment variable resolver during execution." },
|
||||
{ question: "What happens if I reference a block that did not execute on the current path?", answer: "If the referenced block exists in the workflow but did not produce output (for example, it was on an unselected condition branch), the reference resolves to an empty value. In most blocks this becomes an empty string; in Function blocks it becomes null." },
|
||||
{ question: "Can I access array elements inside a tag reference?", answer: "Yes. Use bracket notation for array indices within the dot path, for example <api1.data.users[0].name>. The resolver supports multiple levels of array indexing like items[0][1] as well." },
|
||||
{ question: "How are tag values formatted inside Function blocks versus other blocks?", answer: "In Function blocks, resolved values are formatted as code literals (strings are quoted, objects are JSON, null stays as null) so they can be used directly in JavaScript or Python code. In other block types, objects are JSON-stringified and primitives are converted to plain strings." },
|
||||
{ question: "Can I mix static text with tag references?", answer: "Yes. You can embed tag references anywhere in a text string, such as \"Hello, <agent1.content>! Your order total is <api1.data.total>.\" The resolver replaces each tag in place while leaving the surrounding text intact." },
|
||||
]} />
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ title: Copilot
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { MessageCircle, Hammer, Zap, Globe, Paperclip, History, RotateCcw, Brain } from 'lucide-react'
|
||||
import { MessageCircle, Hammer, ListChecks, Zap, Globe, Paperclip, History, RotateCcw, Brain } from 'lucide-react'
|
||||
|
||||
Copilot is your in-editor assistant that helps you build and edit workflows. It can:
|
||||
|
||||
@@ -49,6 +49,18 @@ Switch between modes using the mode selector at the bottom of the input area.
|
||||
Workflow building mode. Copilot can add blocks, wire connections, edit configurations, and debug issues.
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<ListChecks className="h-4 w-4 text-muted-foreground" />
|
||||
Plan
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="m-0 text-sm">
|
||||
Creates a step-by-step implementation plan for your workflow without making any changes. Helps you think through the approach before building.
|
||||
</div>
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## Models
|
||||
@@ -185,10 +197,10 @@ Selected options are highlighted; unselected options appear struck through.
|
||||
|
||||
## Usage Limits
|
||||
|
||||
Copilot usage is billed per token from the underlying LLM. If you reach your usage limit, Copilot will prompt you to increase your limit. You can add usage in increments ($50, $100) from your current base.
|
||||
Copilot usage is billed per token from the underlying LLM and counts toward your plan's credit usage. If you reach your usage limit, enable on-demand billing from Settings → Subscription to continue using Copilot beyond your plan's included credits.
|
||||
|
||||
<Callout type="info">
|
||||
See the [Cost Calculation page](/execution/costs) for billing details.
|
||||
See the [Cost Calculation page](/execution/costs) for billing and plan details.
|
||||
</Callout>
|
||||
## Copilot MCP
|
||||
|
||||
@@ -286,3 +298,15 @@ Replace `YOUR_COPILOT_API_KEY` with your key.
|
||||
For self-hosted deployments, replace `https://www.sim.ai` with your self-hosted Sim URL.
|
||||
</Callout>
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What is the difference between Ask, Build, and Plan mode?", answer: "Copilot has three modes. Ask mode is a read-only Q&A mode for explanations, guidance, and suggestions without making any changes to your workflow. Build mode allows Copilot to actively modify your workflow by adding blocks, wiring connections, editing configurations, and debugging issues. Plan mode creates a step-by-step implementation plan for your request without making any changes, so you can review the approach before committing. Use Ask when you want to learn or explore ideas, Plan when you want to see a proposed approach first, and Build when you want Copilot to make changes directly." },
|
||||
{ question: "Does Copilot have access to my full workflow when answering questions?", answer: "Copilot has access to the workflow you are currently editing as context. You can also use the @ context menu to reference other workflows, previous chats, execution logs, knowledge bases, documentation, and templates to give Copilot additional context for your request." },
|
||||
{ question: "How do I use Copilot from an external editor like Cursor or VS Code?", answer: "You can use Copilot as an MCP server from external editors. First, generate a Copilot API key from Settings > Copilot on sim.ai. Then add the MCP server configuration to your editor using the endpoint https://www.sim.ai/api/mcp/copilot with your API key in the X-API-Key header. Configuration examples are available for Cursor, Claude Code, Claude Desktop, and VS Code." },
|
||||
{ question: "Can I revert changes that Copilot made to my workflow?", answer: "Yes. When Copilot makes changes in Build mode, it saves checkpoints of your workflow state. You can hover over a Copilot message and click the checkpoints icon to see saved states, then click Revert on any checkpoint to restore your workflow. Note that reverting cannot be undone, so review the checkpoint before confirming." },
|
||||
{ question: "How does Copilot billing work?", answer: "Copilot usage is billed per token from the underlying LLM and counts toward your plan's credit usage. More capable models like Claude Opus cost more per token than lighter models like Haiku. If you reach your usage limit, you can enable on-demand billing from Settings > Subscription to continue using Copilot." },
|
||||
{ question: "What do the slash commands like /research and /search do?", answer: "Slash commands trigger specialized behaviors. /fast enables fast mode execution, /research activates a research and exploration mode, and /actions executes agent actions. Web commands like /search, /read, /scrape, and /crawl let Copilot interact with the web to search for information, read URLs, scrape page content, or crawl multiple pages to gather context for your request." },
|
||||
{ question: "How do I set up Copilot for a self-hosted deployment?", answer: "For self-hosted deployments, go to sim.ai > Settings > Copilot and generate a Copilot API key. Then set the COPILOT_API_KEY environment variable in your self-hosted environment. Copilot is a Sim-managed service, so the self-hosted instance communicates with Sim's servers to process requests." },
|
||||
]} />
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ description: Manage secrets, API keys, and OAuth connections for your workflows
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Credentials provide a secure way to manage API keys, tokens, and third-party service connections across your workflows. Instead of hardcoding sensitive values into your workflow, you store them as credentials and reference them at runtime.
|
||||
|
||||
@@ -190,3 +191,13 @@ To share a credential with specific team members:
|
||||
- **Name keys descriptively** — `STRIPE_SECRET_KEY` over `KEY1`
|
||||
- **Connect multiple OAuth accounts** when you need different permissions or identities per workflow
|
||||
- **Never hardcode secrets** in workflow input fields — always use `{{KEY}}` references
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Are my secrets encrypted at rest?", answer: "Yes. Secret values and OAuth tokens are encrypted before being stored in the database. The platform uses server-side encryption so that raw secret values are never persisted in plaintext. Secret values are also never exposed in the workflow editor, logs, or API responses." },
|
||||
{ question: "What happens if both a workspace secret and a personal secret have the same key name?", answer: "The workspace secret takes precedence. During execution, the resolver checks workspace secrets first and uses personal secrets only as a fallback. This ensures that production workflows use the shared, team-managed value." },
|
||||
{ question: "Who determines which personal secret is used for automated runs?", answer: "For manual runs, the personal secrets of the user who clicked Run are used as fallback. For automated runs triggered by API, webhook, or schedule, the personal secrets of the workflow owner are used instead." },
|
||||
{ question: "Does Sim handle OAuth token refresh automatically?", answer: "Yes. When an OAuth token is used during execution, the platform checks whether the access token has expired and automatically refreshes it using the stored refresh token before making the API call. You do not need to handle token refresh manually." },
|
||||
{ question: "Can I connect multiple OAuth accounts for the same provider?", answer: "Yes. You can connect multiple accounts per provider (for example, two separate Gmail accounts). Each block that requires OAuth lets you select which specific account to use from the credential dropdown. This is useful when different workflows or blocks need different permissions or identities." },
|
||||
{ question: "What happens if I delete a credential that is used in a workflow?", answer: "If a block references a deleted credential, the workflow will fail at that block during execution because the credential cannot be resolved. Make sure to update any blocks that reference a credential before deleting it." },
|
||||
{ question: "Can I import secrets from a .env file?", answer: "Yes. The bulk import feature lets you paste .env-style content in KEY=VALUE format. The parser supports quoted values, comments (lines starting with #), and blank lines. All imported secrets are created with the scope you choose (workspace or personal)." },
|
||||
]} />
|
||||
|
||||
@@ -4,6 +4,7 @@ description: Enterprise features for business organizations
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Sim Enterprise provides advanced features for organizations with enhanced security, compliance, and management requirements.
|
||||
|
||||
@@ -108,3 +109,14 @@ curl -X DELETE "https://your-instance/api/v1/admin/workspaces/{workspaceId}/memb
|
||||
|
||||
- Enabling `ACCESS_CONTROL_ENABLED` automatically enables organizations, as access control requires organization membership.
|
||||
- When `DISABLE_INVITATIONS` is set, users cannot send invitations. Use the Admin API to manage workspace and organization memberships instead.
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What are the minimum requirements to self-host Sim?", answer: "The Docker Compose production setup includes the Sim application (8 GB memory limit), a realtime collaboration server (1 GB memory limit), and a PostgreSQL database with pgvector. A machine with at least 16 GB of RAM and 4 CPU cores is recommended. You will also need Docker and Docker Compose installed." },
|
||||
{ question: "Can I run Sim completely offline with local AI models?", answer: "Yes. Sim supports Ollama and VLLM for running local AI models. A separate Docker Compose configuration (docker-compose.ollama.yml) is available for deploying with Ollama. This lets you run workflows without any external API calls, keeping all data on your infrastructure." },
|
||||
{ question: "How does data privacy work with self-hosted deployments?", answer: "When self-hosted, all data stays on your infrastructure. Workflow definitions, execution logs, credentials, and user data are stored in your PostgreSQL database. If you use local AI models through Ollama or VLLM, no data leaves your network. When using external AI providers, only the data sent in prompts goes to those providers." },
|
||||
{ question: "Do I need a paid license to self-host Sim?", answer: "The core Sim platform is open source under Apache 2.0 and can be self-hosted for free. Enterprise features like SSO (SAML/OIDC), access control with permission groups, and organization management require an Enterprise subscription for production use. These features can be enabled via environment variables for development and evaluation without a license." },
|
||||
{ question: "Which SSO providers are supported?", answer: "Sim supports SAML 2.0 and OIDC protocols, which means it works with virtually any enterprise identity provider including Okta, Azure AD (Entra ID), Google Workspace, and OneLogin. Configuration is done through Settings in the workspace UI." },
|
||||
{ question: "How do I manage users when invitations are disabled?", answer: "Use the Admin API with your admin API key. You can create organizations, add members to organizations with specific roles, add users to workspaces with defined permissions, and remove users. All management is done through REST API calls authenticated with the x-admin-key header." },
|
||||
{ question: "Can I scale Sim horizontally for high availability?", answer: "The Docker Compose setup is designed for single-node deployments. For production scaling, you can deploy on Kubernetes with multiple application replicas behind a load balancer. The database can be scaled independently using managed PostgreSQL services. Redis can be configured for session and cache management across multiple instances." },
|
||||
{ question: "How do access control permission groups work?", answer: "Permission groups let you restrict which AI providers, workflow blocks, and platform features are available to specific team members. Users not assigned to any group have full access. Restrictions are enforced at both the UI level (hiding restricted options) and at execution time (blocking unauthorized operations). Enabling access control automatically enables organization management." },
|
||||
]} />
|
||||
|
||||
@@ -17,7 +17,7 @@ curl -H "x-api-key: YOUR_API_KEY" \
|
||||
https://sim.ai/api/v1/logs?workspaceId=YOUR_WORKSPACE_ID
|
||||
```
|
||||
|
||||
You can generate API keys from your user settings in the Sim dashboard.
|
||||
You can generate API keys from the Sim platform and navigate to **Settings**, then go to **Sim Keys** and click **Create**.
|
||||
|
||||
## Logs API
|
||||
|
||||
@@ -592,3 +592,15 @@ app.listen(3000, () => {
|
||||
console.log('Webhook server listening on port 3000');
|
||||
});
|
||||
```
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How do I trigger async execution via the API?", answer: "Set the X-Execution-Mode header to 'async' on your POST request to /api/workflows/{id}/execute. The API returns a 202 response with a jobId, executionId, and a statusUrl you can poll to check when the job completes. Async mode does not support draft state, workflow overrides, or selective output options." },
|
||||
{ question: "What authentication methods does the API support?", answer: "The API supports two authentication methods: API keys passed in the x-api-key header, and session-based authentication for logged-in users. API keys can be generated from Settings > Sim Keys in the platform. Workflows with public API access enabled can also be called without authentication." },
|
||||
{ question: "How does the webhook retry policy work?", answer: "Failed webhook deliveries are retried up to 5 times with exponential backoff: 5 seconds, 15 seconds, 1 minute, 3 minutes, and 10 minutes, plus up to 10% jitter. Only HTTP 5xx and 429 responses trigger retries. Each delivery times out after 30 seconds." },
|
||||
{ question: "What rate limits apply to the Logs API?", answer: "Rate limits use a token bucket algorithm. Free plans get 30 requests/minute with 60 burst capacity, Pro gets 100/200, Team gets 200/400, and Enterprise gets 500/1000. These are separate from workflow execution rate limits, which are shown in the response body." },
|
||||
{ question: "How do I verify that a webhook is from Sim?", answer: "Configure a webhook secret when setting up notifications. Sim signs each delivery with HMAC-SHA256 using the format 't={timestamp},v1={signature}' in the sim-signature header. Compute the HMAC of '{timestamp}.{body}' with your secret and compare it to the signature value." },
|
||||
{ question: "What alert rules are available for notifications?", answer: "You can configure alerts for consecutive failures, failure rate thresholds, latency thresholds, latency spikes (percentage above average), cost thresholds, no-activity periods, and error counts within a time window. All alert types include a 1-hour cooldown to prevent notification spam." },
|
||||
{ question: "Can I filter which executions trigger notifications?", answer: "Yes. You can filter notifications by specific workflows (or select all), log level (info or error), and trigger type (api, webhook, schedule, manual, chat). You can also choose whether to include final output, trace spans, rate limits, and usage data in the notification payload." },
|
||||
]} />
|
||||
|
||||
@@ -8,16 +8,22 @@ import { Image } from '@/components/ui/image'
|
||||
|
||||
Sim automatically calculates costs for all workflow executions, providing transparent pricing based on AI model usage and execution charges. Understanding these costs helps you optimize workflows and manage your budget effectively.
|
||||
|
||||
## Credits
|
||||
|
||||
Sim uses **credits** as the unit of measurement for all usage. **1 credit = $0.005**.
|
||||
|
||||
All plan limits, usage meters, and billing thresholds are displayed in credits throughout the Sim UI. Dollar amounts in this documentation are provided for reference.
|
||||
|
||||
## How Costs Are Calculated
|
||||
|
||||
Every workflow execution includes two cost components:
|
||||
|
||||
**Base Execution Charge**: $0.005 per execution
|
||||
**Base Execution Charge**: 1 credit ($0.005) per execution
|
||||
|
||||
**AI Model Usage**: Variable cost based on token consumption
|
||||
```javascript
|
||||
modelCost = (inputTokens × inputPrice + outputTokens × outputPrice) / 1,000,000
|
||||
totalCost = baseExecutionCharge + modelCost
|
||||
totalCredits = baseExecutionCharge + modelCost × 200
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
@@ -129,22 +135,131 @@ Use your own API keys for AI model providers instead of Sim's hosted keys to pay
|
||||
|
||||
When configured, workflows use your key instead of Sim's hosted keys. If removed, workflows automatically fall back to hosted keys with the multiplier.
|
||||
|
||||
## Cost Optimization Strategies
|
||||
## Plans
|
||||
|
||||
- **Model Selection**: Choose models based on task complexity. Simple tasks can use GPT-4.1-nano while complex reasoning might need o1 or Claude Opus.
|
||||
- **Prompt Engineering**: Well-structured, concise prompts reduce token usage without sacrificing quality.
|
||||
- **Local Models**: Use Ollama or VLLM for non-critical tasks to eliminate API costs entirely.
|
||||
- **Caching and Reuse**: Store frequently used results in variables or files to avoid repeated AI model calls.
|
||||
- **Batch Processing**: Process multiple items in a single AI request rather than making individual calls.
|
||||
Sim has two paid plan tiers — **Pro** and **Max**. Either can be used individually or with a team. Team plans pool credits across all seats in the organization.
|
||||
|
||||
| Plan | Price | Credits Included | Daily Refresh |
|
||||
|------|-------|------------------|---------------|
|
||||
| **Community** | $0 | 1,000 (one-time) | — |
|
||||
| **Pro** | $25/mo | 6,000/mo | +50/day |
|
||||
| **Max** | $100/mo | 25,000/mo | +200/day |
|
||||
| **Enterprise** | Custom | Custom | — |
|
||||
|
||||
To use Pro or Max with a team, select **Get For Team** in subscription settings and choose the tier and number of seats. Credits are pooled across the organization at the per-seat rate (e.g. Max for Teams with 3 seats = 75,000 credits/mo pooled).
|
||||
|
||||
### Daily Refresh Credits
|
||||
|
||||
Paid plans include a small daily credit allowance that does not count toward your plan limit. Each day, usage up to the daily refresh amount is excluded from billable usage. This allowance resets every 24 hours and does not carry over — use it or lose it.
|
||||
|
||||
| Plan | Daily Refresh |
|
||||
|------|---------------|
|
||||
| **Pro** | 50 credits/day ($0.25) |
|
||||
| **Max** | 200 credits/day ($1.00) |
|
||||
|
||||
For team plans, the daily refresh scales with seats (e.g. Max for Teams with 3 seats = 600 credits/day).
|
||||
|
||||
### Annual Billing
|
||||
|
||||
All paid plans are available with annual billing at a **15% discount**. Switch between monthly and annual billing in Settings → Subscription.
|
||||
|
||||
| Plan | Monthly | Annual (per month) | Annual Total |
|
||||
|------|---------|-------------------|--------------|
|
||||
| **Pro** | $25/mo | $21.25/mo | $255/yr |
|
||||
| **Max** | $100/mo | $85/mo | $1,020/yr |
|
||||
|
||||
Team plans follow the same pricing per seat.
|
||||
|
||||
### On-Demand Billing
|
||||
|
||||
By default, your usage is capped at the credits included in your plan. To allow usage beyond your plan's included amount, you can either enable **on-demand billing** or manually edit your usage limit to any value above your plan's minimum.
|
||||
|
||||
- **Enable On-Demand**: Removes the usage cap entirely. You pay for any overage at the end of the billing period.
|
||||
- **Edit Usage Limit**: Set a specific cap above your plan's included amount to control how much overage you're willing to allow.
|
||||
- **Disable On-Demand**: Resets your usage limit back to the plan's included amount (only available if your current usage hasn't already exceeded it).
|
||||
|
||||
<Callout type="info">
|
||||
On-demand billing is managed by workspace admins for team plans. Non-admin team members cannot toggle on-demand billing.
|
||||
</Callout>
|
||||
|
||||
## Plan Limits
|
||||
|
||||
### Rate Limits
|
||||
|
||||
| Plan | Sync (req/min) | Async (req/min) |
|
||||
|------|----------------|-----------------|
|
||||
| **Free** | 50 | 200 |
|
||||
| **Pro** | 150 | 1,000 |
|
||||
| **Max** | 300 | 2,500 |
|
||||
| **Enterprise** | 600 | 5,000 |
|
||||
|
||||
Max (individual) shares the same rate limits as team plans. Team plans (Pro or Max for Teams) use the Max-tier rate limits.
|
||||
|
||||
### File Storage
|
||||
|
||||
| Plan | Storage |
|
||||
|------|---------|
|
||||
| **Free** | 5 GB |
|
||||
| **Pro** | 50 GB |
|
||||
| **Max** | 500 GB |
|
||||
| **Enterprise** | 500 GB (customizable) |
|
||||
|
||||
Team plans (Pro or Max for Teams) use 500 GB.
|
||||
|
||||
### Execution Time Limits
|
||||
|
||||
| Plan | Sync | Async |
|
||||
|------|------|-------|
|
||||
| **Free** | 5 minutes | 90 minutes |
|
||||
| **Pro / Max / Team / Enterprise** | 50 minutes | 90 minutes |
|
||||
|
||||
**Sync executions** run immediately and return results directly. These are triggered via the API with `async: false` (default) or through the UI.
|
||||
**Async executions** (triggered via API with `async: true`, webhooks, or schedules) run in the background.
|
||||
|
||||
<Callout type="info">
|
||||
If a workflow exceeds its time limit, it will be terminated and marked as failed with a timeout error. Design long-running workflows to use async execution or break them into smaller workflows.
|
||||
</Callout>
|
||||
|
||||
## Billing Model
|
||||
|
||||
Sim uses a **base subscription + overage** billing model:
|
||||
|
||||
### How It Works
|
||||
|
||||
**Pro Plan ($25/month — 6,000 credits):**
|
||||
- Monthly subscription includes 6,000 credits of usage
|
||||
- Usage under 6,000 credits → No additional charges
|
||||
- Usage over 6,000 credits (with on-demand enabled) → Pay the overage at month end
|
||||
- Example: 7,000 credits used = $25 (subscription) + $5 (overage for 1,000 extra credits at $0.005/credit)
|
||||
|
||||
**Team Plans:**
|
||||
- Usage is pooled across all team members in the organization
|
||||
- Overage is calculated from total team usage against the pooled limit
|
||||
- Organization owner receives one bill
|
||||
|
||||
**Enterprise Plans:**
|
||||
- Fixed monthly price, no overages
|
||||
- Custom usage limits per agreement
|
||||
|
||||
### Threshold Billing
|
||||
|
||||
When on-demand is enabled and unbilled overage reaches $50, Sim automatically bills the full unbilled amount.
|
||||
|
||||
**Example:**
|
||||
- Day 10: $70 overage → Bill $70 immediately
|
||||
- Day 15: Additional $35 usage ($105 total) → Already billed, no action
|
||||
- Day 20: Another $50 usage ($155 total, $85 unbilled) → Bill $85 immediately
|
||||
|
||||
This spreads large overage charges throughout the month instead of one large bill at period end.
|
||||
|
||||
## Usage Monitoring
|
||||
|
||||
Monitor your usage and billing in Settings → Subscription:
|
||||
|
||||
- **Current Usage**: Real-time usage and costs for the current period
|
||||
- **Usage Limits**: Plan limits with visual progress indicators
|
||||
- **Billing Details**: Projected charges and minimum commitments
|
||||
- **Plan Management**: Upgrade options and billing history
|
||||
- **Current Usage**: Real-time credit usage for the current billing period
|
||||
- **Usage Limits**: Plan limits with a visual progress bar
|
||||
- **On-Demand Billing**: Toggle on-demand billing to allow usage beyond your plan's included credits
|
||||
- **Plan Management**: Upgrade, downgrade, or switch between monthly and annual billing
|
||||
|
||||
### Programmatic Usage Tracking
|
||||
|
||||
@@ -187,7 +302,7 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
|
||||
"usage": {
|
||||
"currentPeriodCost": 12.34,
|
||||
"limit": 100,
|
||||
"plan": "pro"
|
||||
"plan": "pro_6000"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -198,83 +313,33 @@ curl -X GET -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" htt
|
||||
- `remaining`: Current tokens available (can be up to `maxBurst`)
|
||||
|
||||
**Response Fields:**
|
||||
- `currentPeriodCost` reflects usage in the current billing period
|
||||
- `limit` is derived from individual limits (Free/Pro) or pooled organization limits (Team/Enterprise)
|
||||
- `currentPeriodCost` reflects usage in the current billing period (in dollars)
|
||||
- `limit` is derived from individual limits (Free/Pro/Max) or pooled organization limits (Team/Enterprise)
|
||||
- `plan` is the highest-priority active plan associated with your user
|
||||
|
||||
## Plan Limits
|
||||
## Cost Optimization Strategies
|
||||
|
||||
Different subscription plans have different usage limits:
|
||||
|
||||
| Plan | Monthly Usage Included | Rate Limits (per minute) |
|
||||
|------|------------------------|-------------------------|
|
||||
| **Free** | $20 | 50 sync, 200 async |
|
||||
| **Pro** | $20 (adjustable) | 150 sync, 1,000 async |
|
||||
| **Team** | $40/seat (pooled, adjustable) | 300 sync, 2,500 async |
|
||||
| **Enterprise** | Custom | Custom |
|
||||
|
||||
## Execution Time Limits
|
||||
|
||||
Workflows have maximum execution time limits based on your subscription plan:
|
||||
|
||||
| Plan | Sync Execution | Async Execution |
|
||||
|------|----------------|-----------------|
|
||||
| **Free** | 5 minutes | 10 minutes |
|
||||
| **Pro** | 50 minutes | 90 minutes |
|
||||
| **Team** | 50 minutes | 90 minutes |
|
||||
| **Enterprise** | 50 minutes | 90 minutes |
|
||||
|
||||
**Sync executions** run immediately and return results directly. These are triggered via the API with `async: false` (default) or through the UI.
|
||||
**Async executions** (triggered via API with `async: true`, webhooks, or schedules) run in the background. Async time limits are up to 2x the sync limit, capped at 90 minutes.
|
||||
|
||||
|
||||
<Callout type="info">
|
||||
If a workflow exceeds its time limit, it will be terminated and marked as failed with a timeout error. Design long-running workflows to use async execution or break them into smaller workflows.
|
||||
</Callout>
|
||||
|
||||
## Billing Model
|
||||
|
||||
Sim uses a **base subscription + overage** billing model:
|
||||
|
||||
### How It Works
|
||||
|
||||
**Pro Plan ($20/month):**
|
||||
- Monthly subscription includes $20 of usage
|
||||
- Usage under $20 → No additional charges
|
||||
- Usage over $20 → Pay the overage at month end
|
||||
- Example: $35 usage = $20 (subscription) + $15 (overage)
|
||||
|
||||
**Team Plan ($40/seat/month):**
|
||||
- Pooled usage across all team members
|
||||
- Overage calculated from total team usage
|
||||
- Organization owner receives one bill
|
||||
|
||||
**Enterprise Plans:**
|
||||
- Fixed monthly price, no overages
|
||||
- Custom usage limits per agreement
|
||||
|
||||
### Threshold Billing
|
||||
|
||||
When unbilled overage reaches $50, Sim automatically bills the full unbilled amount.
|
||||
|
||||
**Example:**
|
||||
- Day 10: $70 overage → Bill $70 immediately
|
||||
- Day 15: Additional $35 usage ($105 total) → Already billed, no action
|
||||
- Day 20: Another $50 usage ($155 total, $85 unbilled) → Bill $85 immediately
|
||||
|
||||
This spreads large overage charges throughout the month instead of one large bill at period end.
|
||||
|
||||
## Cost Management Best Practices
|
||||
|
||||
1. **Monitor Regularly**: Check your usage dashboard frequently to avoid surprises
|
||||
2. **Set Budgets**: Use plan limits as guardrails for your spending
|
||||
3. **Optimize Workflows**: Review high-cost executions and optimize prompts or model selection
|
||||
4. **Use Appropriate Models**: Match model complexity to task requirements
|
||||
5. **Batch Similar Tasks**: Combine multiple requests when possible to reduce overhead
|
||||
- **Model Selection**: Choose models based on task complexity. Simple tasks can use GPT-4.1-nano while complex reasoning might need o1 or Claude Opus.
|
||||
- **Prompt Engineering**: Well-structured, concise prompts reduce token usage without sacrificing quality.
|
||||
- **Local Models**: Use Ollama or VLLM for non-critical tasks to eliminate API costs entirely.
|
||||
- **Caching and Reuse**: Store frequently used results in variables or files to avoid repeated AI model calls.
|
||||
- **Batch Processing**: Process multiple items in a single AI request rather than making individual calls.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Review your current usage in [Settings → Subscription](https://sim.ai/settings/subscription)
|
||||
- Learn about [Logging](/execution/logging) to track execution details
|
||||
- Explore the [External API](/execution/api) for programmatic cost monitoring
|
||||
- Check out [workflow optimization techniques](/blocks) to reduce costs
|
||||
- Check out [workflow optimization techniques](/blocks) to reduce costs
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How much does a single workflow execution cost?", answer: "Every execution incurs a base charge of 1 credit ($0.005). On top of that, any AI model usage is billed based on token consumption. Workflows that do not use AI blocks only pay the base execution charge." },
|
||||
{ question: "What is the credit-to-dollar conversion rate?", answer: "1 credit equals $0.005. All plan limits, usage meters, and billing thresholds in the Sim UI are displayed in credits." },
|
||||
{ question: "Do unused daily refresh credits carry over?", answer: "No. Daily refresh credits reset every 24 hours and do not accumulate. If you do not use them within the day, they are lost." },
|
||||
{ question: "What happens when I exceed my plan's credit limit?", answer: "By default, your usage is capped at your plan's included credits and executions will stop. If you enable on-demand billing or manually raise your usage limit in Settings, you can continue running workflows and pay for the overage at the end of the billing period." },
|
||||
{ question: "How does the 1.1x hosted model multiplier work?", answer: "When you use Sim's hosted API keys (instead of bringing your own), a 1.1x multiplier is applied to the base model pricing for Agent blocks. This covers infrastructure and API management costs. You can avoid this multiplier by using your own API keys via the BYOK feature." },
|
||||
{ question: "Are there any free options for AI models?", answer: "Yes. If you run local models through Ollama or VLLM, there are no API costs for those model calls. You still pay the base execution charge of 1 credit per execution." },
|
||||
{ question: "When does threshold billing trigger?", answer: "When on-demand billing is enabled and your unbilled overage reaches $50, Sim automatically bills the full unbilled amount. This spreads large charges throughout the month instead of accumulating one large bill at period end." },
|
||||
]} />
|
||||
|
||||
@@ -166,3 +166,14 @@ Use `url` for direct downloads or `base64` for inline processing.
|
||||
2. **Check file types** - Ensure the file type matches what the receiving block expects. The Vision block needs images, the File block handles documents.
|
||||
|
||||
3. **Consider file size** - Large files increase execution time. For very large files, consider using storage blocks (S3, Supabase) for intermediate storage.
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What is the maximum file size for uploads?", answer: "The maximum file size for files processed during workflow execution is 20 MB. Files exceeding this limit will be rejected with an error indicating the actual file size. For larger files, use storage blocks like S3 or Supabase for intermediate storage." },
|
||||
{ question: "What file input formats are supported via the API?", answer: "When triggering a workflow via API, you can send files as base64-encoded data (using a data URI with the format 'data:{mime};base64,{data}') or as a URL pointing to a publicly accessible file. In both cases, include the file name and MIME type in the request." },
|
||||
{ question: "How are files passed between blocks internally?", answer: "Files are represented as standardized UserFile objects with name, url, base64, type, and size properties. Most blocks accept the full file object and extract what they need automatically, so you typically pass the entire object rather than individual properties." },
|
||||
{ question: "Which blocks can output files?", answer: "Gmail outputs email attachments, Slack outputs downloaded files, TTS generates audio files, Video Generator and Image Generator produce media files. Storage blocks like S3, Supabase, Google Drive, and Dropbox can also retrieve files for use in downstream blocks." },
|
||||
{ question: "Do I need to extract base64 or URL from file objects manually?", answer: "No. Most blocks accept the full file object and handle the format conversion automatically. Simply pass the entire file reference (e.g., <gmail.attachments[0]>) and the receiving block will extract the data it needs." },
|
||||
{ question: "How do file fields work in the Start block's input format?", answer: "When you define a field with type 'file[]' in the Start block's input format, the execution engine automatically processes incoming file data (base64 or URL) and uploads it to storage, converting it into UserFile objects before the workflow runs." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Overview
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Sim's execution engine brings your workflows to life by processing blocks in the correct order, managing data flow, and handling errors gracefully, so you can understand exactly how workflows are executed in Sim.
|
||||
|
||||
@@ -55,7 +56,7 @@ Each workflow maintains a rich context during execution containing:
|
||||
|
||||
## Deployment Snapshots
|
||||
|
||||
All public entry points—API, Chat, Schedule, Webhook, and Manual runs—execute the workflow’s active deployment snapshot. Publish a new deployment whenever you change the canvas so every trigger uses the updated version.
|
||||
API, Chat, Schedule, and Webhook executions run against the workflow’s active deployment snapshot. Manual runs from the editor execute the current draft canvas state, letting you test changes before deploying. Publish a new deployment whenever you change the canvas so every trigger uses the updated version.
|
||||
|
||||
<div className='flex justify-center my-6'>
|
||||
<Image
|
||||
@@ -114,3 +115,13 @@ const result = await client.executeWorkflow('workflow-id', {
|
||||
## What's Next?
|
||||
|
||||
Start with [Execution Basics](/execution/basics) to understand how workflows run, then explore [Logging](/execution/logging) to monitor your executions and [Cost Calculation](/execution/costs) to optimize your spending.
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What are the execution timeout limits?", answer: "Synchronous executions (API, chat) have a default timeout of 5 minutes on the Free plan and 50 minutes on Pro, Team, and Enterprise plans. Asynchronous executions (schedules, webhooks) allow up to 90 minutes across all plans. These limits are configurable by the platform administrator." },
|
||||
{ question: "How does parallel execution work?", answer: "The engine identifies layers of blocks with no dependencies on each other and runs them concurrently. Within loops and parallel blocks, the engine supports up to 20 parallel branches by default and up to 1,000 loop iterations. Nested subflows (loops inside parallels, or vice versa) are supported up to 10 levels deep." },
|
||||
{ question: "Can I cancel a running execution?", answer: "Yes. The engine supports cancellation through an abort signal mechanism. When you cancel an execution, the engine checks for cancellation between block executions (at roughly 500ms intervals when using Redis-backed cancellation). Any in-progress blocks complete, and the execution returns with a cancelled status." },
|
||||
{ question: "What is a deployment snapshot?", answer: "A deployment snapshot is a frozen copy of your workflow at the time you click Deploy. Trigger-based executions (API, chat, schedule, webhook) run against the active snapshot, not your draft canvas. Manual runs from the editor execute the current draft canvas state, so you can test changes before deploying. You can view, compare, and roll back snapshots from the Deploy modal." },
|
||||
{ question: "How are execution costs calculated?", answer: "Costs are tracked per block based on the AI model used. Each block log records input tokens, output tokens, and the computed cost using the model's pricing. The total workflow cost is the sum of all block-level costs for that execution. You can review costs in the execution logs." },
|
||||
{ question: "What happens when a block fails during execution?", answer: "When a block throws an error, the engine captures the error message in the block log, finalizes any incomplete logs with timing data, and halts the execution with a failure status. If the failing block has an error output handle connected to another block, that error path is followed instead of halting entirely." },
|
||||
{ question: "Can I re-run part of a workflow without starting from scratch?", answer: "Yes. The run-from-block feature lets you select a specific block and re-execute from that point. The engine computes which upstream blocks need to be re-run (the dirty set) and preserves cached outputs from blocks that have not changed, so only the affected portion of the workflow re-executes." },
|
||||
]} />
|
||||
|
||||
@@ -121,10 +121,8 @@ The snapshot provides:
|
||||
|
||||
## Log Retention
|
||||
|
||||
- **Free Plan**: 7 days of log retention
|
||||
- **Pro Plan**: 30 days of log retention
|
||||
- **Team Plan**: 90 days of log retention
|
||||
- **Enterprise Plan**: Custom retention periods available
|
||||
- **Free Plan**: 7 days of log retention (logs are archived to cloud storage and then deleted)
|
||||
- **Pro / Team / Enterprise Plans**: Logs are retained indefinitely (no automatic cleanup)
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -147,4 +145,15 @@ The snapshot provides:
|
||||
|
||||
- Learn about [Cost Calculation](/execution/costs) to understand workflow pricing
|
||||
- Explore the [External API](/execution/api) for programmatic log access
|
||||
- Set up [Notifications](/execution/api#notifications) for real-time alerts via webhook, email, or Slack
|
||||
- Set up [Notifications](/execution/api#notifications) for real-time alerts via webhook, email, or Slack
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How long are execution logs retained?", answer: "Free plans retain logs for 7 days — after that, logs are archived to cloud storage and deleted from the database. Pro, Team, and Enterprise plans retain logs indefinitely with no automatic cleanup." },
|
||||
{ question: "What data is captured in each execution log?", answer: "Each log entry includes the execution ID, workflow ID, trigger type, start and end timestamps, total duration in milliseconds, cost breakdown (total cost, token counts, and per-model breakdowns), execution data with trace spans, final output, and any associated files. The log details sidebar lets you inspect block-level inputs and outputs." },
|
||||
{ question: "Are API keys visible in the logs?", answer: "No. API keys and credentials are automatically redacted in the log input tab for security. You can safely inspect block inputs without exposing sensitive values." },
|
||||
{ question: "What is a workflow snapshot?", answer: "A workflow snapshot is a frozen copy of the workflow's structure (blocks, connections, and configuration) captured at execution time. It lets you see the exact state of the workflow when a particular execution ran, which is useful for debugging workflows that have been modified since the execution." },
|
||||
{ question: "Can I access logs programmatically?", answer: "Yes. The External API provides endpoints to query logs with filtering by workflow, time range, trigger type, duration, cost, and model. You can also set up webhook, email, or Slack notifications for real-time alerts when executions complete." },
|
||||
{ question: "What does Live mode do on the Logs page?", answer: "Live mode automatically refreshes the Logs page in real-time so new execution entries appear as they are logged, without requiring manual page refreshes. This is useful during deployments or when monitoring active workflows." },
|
||||
]} />
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '@/components/icons'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Build your first AI workflow in 10 minutes. In this tutorial, you'll create a people research agent that uses advanced LLM-powered search tools to extract and structure information about individuals.
|
||||
|
||||
@@ -177,7 +178,7 @@ Build, test, and refine workflows quickly with immediate feedback
|
||||
Discover API, Function, Condition, and other workflow blocks
|
||||
</Card>
|
||||
<Card title="Browse Integrations" href="/tools">
|
||||
Connect 80+ services including Gmail, Slack, Notion, and more
|
||||
Connect 160+ services including Gmail, Slack, Notion, and more
|
||||
</Card>
|
||||
<Card title="Add Custom Logic" href="/blocks/function">
|
||||
Write custom functions for advanced data processing
|
||||
@@ -191,6 +192,16 @@ Build, test, and refine workflows quickly with immediate feedback
|
||||
|
||||
**Need detailed explanations?** Visit the [Blocks documentation](/blocks) for comprehensive guides on each component.
|
||||
|
||||
**Looking for integrations?** Explore the [Tools documentation](/tools) to see all 80+ available integrations.
|
||||
**Looking for integrations?** Explore the [Tools documentation](/tools) to see all 160+ available integrations.
|
||||
|
||||
**Ready to go live?** Learn about [Execution and Deployment](/execution) to make your workflows production-ready.
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How long does the getting started tutorial take?", answer: "About 10 minutes. The tutorial walks you through creating a people research agent with web search tools and structured output. You will have a fully working workflow by the end." },
|
||||
{ question: "Do I need API keys to follow this tutorial?", answer: "You need API keys for the search tools (Exa and Linkup) used in this tutorial. For the AI model, you can either use Sim's hosted keys (included with your plan credits) or bring your own OpenAI API key. If you prefer not to set up search tool keys, you can still build a basic agent workflow without them." },
|
||||
{ question: "Do I need coding experience to complete this tutorial?", answer: "No. The entire tutorial uses the visual drag-and-drop interface. You will connect blocks, configure settings, and test through the chat panel without writing any code." },
|
||||
{ question: "Can I use a different AI model instead of GPT-4o?", answer: "Yes. The Agent block supports models from OpenAI, Anthropic, Google, Groq, Cerebras, DeepSeek, Mistral, xAI, and more. You can select any available model from the dropdown. If you self-host, you can also use local models through Ollama." },
|
||||
{ question: "Can I import workflows from other tools?", answer: "Sim does not currently support importing workflows from other automation platforms. However, you can use the Copilot feature to describe what you want in natural language and have it build the workflow for you, which is often faster than manual recreation." },
|
||||
{ question: "What if my workflow does not produce the expected output?", answer: "Use the Chat panel to test iteratively and inspect outputs from each block. You can click the dropdown to view different block outputs and pinpoint where the issue is. The execution logs (accessible from the Logs tab) show detailed information about each step including token usage, costs, and any errors." },
|
||||
{ question: "Where do I go after completing this tutorial?", answer: "Explore the Blocks documentation to learn about Condition, Router, Function, and API blocks. Browse the Tools section to discover 160+ integrations you can add to your agents. When you are ready to deploy, check the Execution docs for REST API, webhook, and scheduled trigger options." },
|
||||
]} />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Sim is an open-source visual workflow builder for building and deploying AI agent workflows. Design intelligent automation systems using a no-code interface—connect AI models, databases, APIs, and business tools through an intuitive drag-and-drop canvas. Whether you're building chatbots, automating business processes, or orchestrating complex data pipelines, Sim provides the tools to bring your AI workflows to life.
|
||||
|
||||
@@ -57,11 +58,11 @@ Enable your team to build together. Multiple users can edit workflows simultaneo
|
||||
|
||||
## Integrations
|
||||
|
||||
Sim provides native integrations with 80+ services across multiple categories:
|
||||
Sim provides native integrations with 160+ services across multiple categories:
|
||||
|
||||
- **AI Models**: OpenAI, Anthropic, Google Gemini, Groq, Cerebras, local models via Ollama or VLLM
|
||||
- **Communication**: Gmail, Slack, Microsoft Teams, Telegram, WhatsApp
|
||||
- **Productivity**: Notion, Google Workspace, Airtable, Monday.com
|
||||
- **Productivity**: Notion, Google Workspace, Airtable
|
||||
- **Development**: GitHub, Jira, Linear, automated browser testing
|
||||
- **Search & Data**: Google Search, Perplexity, Firecrawl, Exa AI
|
||||
- **Databases**: PostgreSQL, MySQL, Supabase, Pinecone, Qdrant
|
||||
@@ -109,9 +110,20 @@ Ready to build your first AI workflow?
|
||||
Learn about the building blocks
|
||||
</Card>
|
||||
<Card title="Tools & Integrations" href="/tools">
|
||||
Explore 80+ built-in integrations
|
||||
Explore 160+ built-in integrations
|
||||
</Card>
|
||||
<Card title="Team Permissions" href="/permissions/roles-and-permissions">
|
||||
Set up workspace roles and permissions
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Is Sim free to use?", answer: "Sim offers a free Community plan with 1,000 one-time credits to get started. Paid plans start at $25/month (Pro) with 5,000 credits and go up to $100/month (Max) with 20,000 credits. Annual billing is available at a 15% discount. You can also self-host Sim for free on your own infrastructure." },
|
||||
{ question: "Is Sim open source?", answer: "Yes. Sim is open source under the Apache 2.0 license. The full source code is available on GitHub and you can self-host it, contribute to development, or modify it for your own needs. Enterprise features (SSO, access control) have a separate license that requires a subscription for production use." },
|
||||
{ question: "Which AI models and providers are supported?", answer: "Sim supports 15+ providers including OpenAI, Anthropic, Google Gemini, Groq, Cerebras, DeepSeek, Mistral, xAI, and OpenRouter. You can also run local models through Ollama or VLLM at no API cost. Bring Your Own Key (BYOK) is supported so you can use your own API keys at base provider pricing with no markup." },
|
||||
{ question: "Do I need coding experience to use Sim?", answer: "No. Sim is a no-code visual builder where you design workflows by dragging blocks onto a canvas and connecting them. For advanced use cases, the Function block lets you write custom JavaScript, but it is entirely optional." },
|
||||
{ question: "Can I self-host Sim?", answer: "Yes. Sim provides Docker Compose configurations for self-hosted deployments. The stack includes the Sim application, a PostgreSQL database with pgvector, and a realtime collaboration server. You can also integrate local AI models via Ollama for a fully offline setup." },
|
||||
{ question: "Is there a limit on how many workflows I can create?", answer: "There is no limit on the number of workflows you can create on any plan. Usage limits apply to execution credits, rate limits, and file storage, which vary by plan tier." },
|
||||
{ question: "What integrations are available?", answer: "Sim offers 160+ native integrations across categories including AI models, communication tools (Gmail, Slack, Teams, Telegram), productivity apps (Notion, Google Workspace, Airtable), development tools (GitHub, Jira, Linear), search services (Google Search, Perplexity, Exa), and databases (PostgreSQL, Supabase, Pinecone). For anything not built in, you can use the MCP (Model Context Protocol) support to connect custom services." },
|
||||
{ question: "How does Sim compare to other workflow automation tools?", answer: "Sim is purpose-built for AI agent workflows rather than general task automation. It provides a visual canvas for orchestrating LLM-powered agents with built-in support for tool use, structured outputs, conditional branching, and real-time collaboration. The Copilot feature also lets you build and modify workflows using natural language." },
|
||||
]} />
|
||||
|
||||
156
apps/docs/content/docs/en/knowledgebase/connectors.mdx
Normal file
156
apps/docs/content/docs/en/knowledgebase/connectors.mdx
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: Connectors
|
||||
description: Automatically sync documents from external sources into your knowledge base
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Connectors let you pull documents directly from external services into your knowledge base. Instead of manually uploading files, a connector continuously syncs content from sources like Notion, Google Drive, GitHub, Slack, and more — keeping your knowledge base up to date automatically.
|
||||
|
||||
## Available Connectors
|
||||
|
||||
Sim ships with 30 built-in connectors spanning productivity tools, cloud storage, development platforms, and more.
|
||||
|
||||
| Category | Connectors |
|
||||
|----------|-----------|
|
||||
| **Productivity** | Notion, Confluence, Asana, Linear, Jira, Google Calendar, Google Sheets |
|
||||
| **Cloud Storage** | Google Drive, Dropbox, OneDrive, SharePoint |
|
||||
| **Documents** | Google Docs, WordPress, Webflow |
|
||||
| **Development** | GitHub |
|
||||
| **Communication** | Slack, Discord, Microsoft Teams, Reddit |
|
||||
| **Email** | Gmail, Outlook |
|
||||
| **CRM** | HubSpot, Salesforce |
|
||||
| **Support** | Intercom, ServiceNow, Zendesk |
|
||||
| **Data** | Airtable |
|
||||
| **Note-taking** | Evernote, Obsidian |
|
||||
| **Meetings** | Fireflies |
|
||||
|
||||
## Adding a Connector
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### Select a source
|
||||
|
||||
Open a knowledge base and click **Add Connector**. You'll see the full list of available connectors — pick the service you want to sync from.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Authenticate
|
||||
|
||||
Most connectors use **OAuth** — select an existing credential from the dropdown, or click **Connect new account** to authorize through the service's login flow. Tokens are refreshed automatically, so you won't need to re-authenticate unless you revoke access.
|
||||
|
||||
A few connectors (Evernote, Obsidian, Fireflies) use **API keys** instead. Paste your key or developer token directly, and it will be stored securely.
|
||||
|
||||
<Callout type="info">
|
||||
If you rotate an API key in the external service, you'll need to update it in Sim as well. OAuth tokens are refreshed automatically, but API keys are not.
|
||||
</Callout>
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Configure
|
||||
|
||||
Each connector has its own configuration fields that control what gets synced. Some examples:
|
||||
|
||||
- **Notion**: Choose between syncing an entire workspace, a specific database, or a single page tree
|
||||
- **GitHub**: Specify a repository, branch, and optional file extension filter
|
||||
- **Confluence**: Enter your Atlassian domain and optionally filter by space key or content type
|
||||
- **Obsidian**: Provide your vault URL and optionally restrict to a folder path
|
||||
|
||||
All configuration is validated when you save — if a repository doesn't exist or a domain is unreachable, you'll get an immediate error.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Choose sync frequency
|
||||
|
||||
Select how often the connector should re-sync:
|
||||
|
||||
| Frequency | Description |
|
||||
|-----------|-------------|
|
||||
| Every hour | Best for fast-moving sources |
|
||||
| Every 6 hours | Good balance for most use cases |
|
||||
| **Daily** (default) | Suitable for content that changes infrequently |
|
||||
| Weekly | For stable, rarely-updated sources |
|
||||
| Manual only | Sync only when you trigger it |
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Configure metadata tags (optional)
|
||||
|
||||
If the connector supports metadata tags, you'll see checkboxes for each tag type (e.g., Labels, Last Modified, Notebook). All are enabled by default — uncheck any you don't need.
|
||||
|
||||
See the [Metadata Tags](#metadata-tags) section below for details.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Connect & Sync
|
||||
|
||||
Click **Connect & Sync** to save the connector and trigger the first sync immediately. Documents will begin appearing in your knowledge base as they are processed.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## How Syncing Works
|
||||
|
||||
On each sync, the connector fetches documents from the external service and compares them against what's already in your knowledge base. Only documents that have actually changed are reprocessed — new content is added, updated content is re-chunked and re-embedded, and documents that no longer exist in the source are removed.
|
||||
|
||||
This means syncing is efficient even for large document sets. A connector with thousands of documents will only do meaningful work when something changes.
|
||||
|
||||
### Handling Failures
|
||||
|
||||
If a single document fails to fetch (e.g., due to a permission issue or timeout), the sync continues with the remaining documents. The failed document will be retried on the next sync cycle.
|
||||
|
||||
If an entire sync fails (e.g., the service is down or credentials expired), the connector automatically backs off and retries later. The backoff resets as soon as a sync succeeds.
|
||||
|
||||
## Metadata Tags
|
||||
|
||||
Connectors can automatically populate [tags](/docs/knowledgebase/tags) with metadata from the source, letting you filter documents in the Knowledge block based on information from the external service.
|
||||
|
||||
For example, a Notion connector might tag documents with their **Labels**, **Last Modified** date, and **Created** date. A GitHub connector might tag documents with their **Repository** and **File Path**. This metadata becomes available for [tag-based filtering](/docs/knowledgebase/tags) in your workflows.
|
||||
|
||||
### Opting Out
|
||||
|
||||
You can disable specific metadata tags during connector setup. Disabled tags won't be populated, leaving those tag slots available for other connectors or manual tagging.
|
||||
|
||||
<Callout type="info">
|
||||
Tag slots are shared across all documents in a knowledge base. If you have multiple connectors, each one's metadata tags draw from the same pool of available slots.
|
||||
</Callout>
|
||||
|
||||
## Excluding Documents
|
||||
|
||||
You can manually exclude specific documents from a connector's sync. Excluded documents are skipped on every subsequent sync, even if they change in the source. This is useful for filtering out templates, drafts, or other content you don't want in your knowledge base.
|
||||
|
||||
## Source Links
|
||||
|
||||
Every synced document retains a link back to the original in the external service. This lets you trace any knowledge base document to its source — whether that's a Notion page, a GitHub file, a Confluence article, or a Slack conversation.
|
||||
|
||||
## Multiple Connectors
|
||||
|
||||
You can add multiple connectors to a single knowledge base. For example, you might sync internal documentation from Confluence alongside code from GitHub and meeting notes from Fireflies — all searchable together through the Knowledge block.
|
||||
|
||||
Each connector manages its own documents independently. Metadata tag slots are shared across the knowledge base, so keep an eye on slot usage if you're combining several connectors that each populate tags.
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
- **Internal knowledge base**: Sync your team's Notion workspace and Confluence spaces so AI agents can answer questions about internal processes, policies, and documentation
|
||||
- **Customer support**: Connect HubSpot or Salesforce alongside your help docs from WordPress or Google Docs to give support agents full context on customers and product information
|
||||
- **Engineering assistant**: Sync a GitHub repository and Jira or Linear issues so an AI agent can reference code, specs, and ticket history when answering developer questions
|
||||
- **Meeting intelligence**: Pull in Fireflies transcripts alongside Slack conversations to build a searchable archive of decisions and discussions
|
||||
- **Research and notes**: Sync Evernote notebooks or an Obsidian vault to make your personal notes available to AI workflows
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How often do connectors sync?", answer: "You can choose from hourly, every 6 hours, daily (default), weekly, or manual-only sync frequencies. Each connector can have its own schedule." },
|
||||
{ question: "What happens if a source document is deleted?", answer: "On the next sync, the connector detects that the document no longer exists in the source and removes it from your knowledge base automatically." },
|
||||
{ question: "Can I connect multiple services to one knowledge base?", answer: "Yes. You can add as many connectors as you need to a single knowledge base. Each connector manages its documents independently." },
|
||||
{ question: "Do I need to re-authenticate connectors?", answer: "OAuth-based connectors refresh tokens automatically. API key-based connectors (Evernote, Obsidian, Fireflies) need manual updates if you rotate the key." },
|
||||
{ question: "What if a connector sync fails?", answer: "If a single document fails, the rest of the sync continues. If the entire sync fails (e.g., service is down), the connector backs off and retries automatically." },
|
||||
{ question: "Can I exclude specific documents from syncing?", answer: "Yes. You can manually exclude documents from any connector. Excluded documents are skipped on every subsequent sync, even if they change in the source." },
|
||||
{ question: "Do metadata tags count against a limit?", answer: "Tag slots are shared across all documents in a knowledge base. If you have multiple connectors, their metadata tags draw from the same pool of available slots." },
|
||||
]} />
|
||||
@@ -5,6 +5,7 @@ description: Upload, process, and search through your documents with intelligent
|
||||
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Image } from '@/components/ui/image'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
The knowledgebase allows you to upload, process, and search through your documents with intelligent vector search and chunking. Documents of various types are automatically processed, embedded, and made searchable. Your documents are intelligently chunked, and you can view, edit, and search through them using natural language queries.
|
||||
|
||||
@@ -25,7 +26,7 @@ The system handles the entire processing pipeline for you:
|
||||
|
||||
## Supported File Types
|
||||
|
||||
Sim supports PDF, Word (DOC/DOCX), plain text (TXT), Markdown (MD), HTML, Excel (XLS/XLSX), PowerPoint (PPT/PPTX), and CSV files. Files can be up to 100MB each, with optimal performance for files under 50MB. You can upload multiple documents simultaneously, and PDF files include OCR processing for scanned documents.
|
||||
Sim supports PDF, Word (DOC/DOCX), plain text (TXT), Markdown (MD), HTML, HTM, Excel (XLS/XLSX), PowerPoint (PPT/PPTX), CSV, JSON, and YAML/YML files. Files can be up to 100MB each, with optimal performance for files under 50MB. You can upload multiple documents simultaneously, and PDF files include OCR processing for scanned documents.
|
||||
|
||||
## Viewing and Editing Chunks
|
||||
|
||||
@@ -40,8 +41,8 @@ When creating a knowledge base, you can configure how documents are split into c
|
||||
| Setting | Unit | Default | Range | Description |
|
||||
|---------|------|---------|-------|-------------|
|
||||
| **Max Chunk Size** | tokens | 1,024 | 100-4,000 | Maximum size of each chunk (1 token ≈ 4 characters) |
|
||||
| **Min Chunk Size** | characters | 1 | 1-2,000 | Minimum chunk size to avoid tiny fragments |
|
||||
| **Overlap** | characters | 200 | 0-500 | Context overlap between consecutive chunks |
|
||||
| **Min Chunk Size** | characters | 100 | 100-2,000 | Minimum chunk size to avoid tiny fragments |
|
||||
| **Overlap** | tokens | 200 | 0-500 | Context overlap between consecutive chunks |
|
||||
|
||||
- **Hierarchical splitting**: Respects document structure (sections, paragraphs, sentences)
|
||||
|
||||
@@ -117,4 +118,14 @@ Sim uses vector search powered by [pgvector](https://github.com/pgvector/pgvecto
|
||||
4. **Explore chunks**: View and edit the processed content
|
||||
5. **Add to workflows**: Use the Knowledge block to integrate with your AI agents
|
||||
|
||||
The knowledgebase transforms your static documents into an intelligent, searchable resource that your AI workflows can leverage for more informed and contextual responses.
|
||||
The knowledgebase transforms your static documents into an intelligent, searchable resource that your AI workflows can leverage for more informed and contextual responses.
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What file types are supported?", answer: "PDF, Word (DOC/DOCX), plain text (TXT), Markdown (MD), HTML, HTM, Excel (XLS/XLSX), PowerPoint (PPT/PPTX), CSV, JSON, and YAML/YML files." },
|
||||
{ question: "Is there a file size limit?", answer: "Files can be up to 100 MB each, with optimal performance for files under 50 MB." },
|
||||
{ question: "Can I edit chunks after processing?", answer: "Yes. You can view, edit, merge, split, and add metadata to individual chunks after documents are processed." },
|
||||
{ question: "How does semantic search work?", answer: "Documents are embedded as vectors using AI models. When you search, your query is also embedded and compared against document vectors to find conceptually similar content — even without exact keyword matches." },
|
||||
{ question: "Does it support scanned PDFs?", answer: "Yes. When configured with Azure or Mistral OCR, Sim can extract text from image-based and scanned PDF documents." },
|
||||
{ question: "Can I search across multiple knowledge bases?", answer: "Each Knowledge block targets a specific knowledge base. You can use multiple Knowledge blocks in a workflow to search across different knowledge bases." },
|
||||
{ question: "How do I control chunk size?", answer: "When creating a knowledge base, you can configure max chunk size (100-4,000 tokens), min chunk size (100-2,000 characters), and overlap (0-500 tokens)." },
|
||||
]} />
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Knowledgebase",
|
||||
"pages": ["index", "tags"]
|
||||
"pages": ["index", "connectors", "tags"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ title: Tags and Filtering
|
||||
---
|
||||
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Tags provide a powerful way to organize your documents and create precise filtering for your vector searches. By combining tag-based filtering with semantic search, you can retrieve exactly the content you need from your knowledgebase.
|
||||
|
||||
@@ -16,7 +17,7 @@ You can add custom tags to any document in your knowledgebase to organize and ca
|
||||
|
||||
### Tag Management
|
||||
- **Custom tags**: Create your own tag system that fits your workflow
|
||||
- **Multiple tags per document**: Apply as many tags as needed to each document, there are 7 tag slots available per knowledgebase that are shared by all documents in the knowledgebase
|
||||
- **Multiple tags per document**: Apply as many tags as needed to each document. Each knowledgebase has 17 tag slots total: 7 text, 5 number, 2 date, and 3 boolean slots, shared by all documents in the knowledgebase
|
||||
- **Tag organization**: Group related documents with consistent tagging
|
||||
|
||||
### Tag Best Practices
|
||||
@@ -65,10 +66,10 @@ When you **provide both tags and a search query**:
|
||||
### Search Configuration
|
||||
|
||||
#### Tag Filtering
|
||||
- **Multiple tags**: Use multiple tags for OR logic (document must have one or more of the tags)
|
||||
- **Multiple tags**: Use multiple tags with AND or OR logic to control whether documents must match all or any of the specified tags
|
||||
- **Tag combinations**: Mix different tag types for precise filtering
|
||||
- **Case sensitivity**: Tag matching is case-insensitive
|
||||
- **Partial matching**: Exact tag name matching required
|
||||
- **Partial matching**: Text fields support partial matching operators such as contains, starts_with, and ends_with in addition to exact matching
|
||||
|
||||
#### Vector Search Parameters
|
||||
- **Query complexity**: Natural language questions work best
|
||||
@@ -105,4 +106,13 @@ When you **provide both tags and a search query**:
|
||||
4. **Integrate into workflows**: Use the Knowledge block with your tagging strategy
|
||||
5. **Refine over time**: Adjust your tagging approach based on search results
|
||||
|
||||
Tags transform your knowledgebase from a simple document store into a precisely organized, searchable intelligence system that your AI workflows can navigate with surgical precision.
|
||||
Tags transform your knowledgebase from a simple document store into a precisely organized, searchable intelligence system that your AI workflows can navigate with surgical precision.
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How many tag slots are available per knowledgebase?", answer: "Each knowledgebase supports up to 17 tag slots total across four field types: 7 text slots, 5 number slots, 2 date slots, and 3 boolean slots. These slots are shared across all documents in the knowledgebase." },
|
||||
{ question: "What tag field types are supported?", answer: "Four field types are supported: text (free-form string values), number (numeric values), date (date values in YYYY-MM-DD format), and boolean (true/false values). Each type has its own pool of available slots." },
|
||||
{ question: "Is tag matching case-sensitive?", answer: "No, tag matching is case-insensitive. You can use any capitalization when filtering by tags and it will match regardless of how the tag value was originally entered." },
|
||||
{ question: "How does combined tag and vector search work?", answer: "When you provide both tags and a search query, tag filtering is applied first to narrow down the document set, then vector search runs within that filtered subset. This approach is more efficient because it reduces the number of vectors that need similarity comparison." },
|
||||
{ question: "What is the default number of results returned from a knowledge search?", answer: "The default is 10 results. You can configure this with the topK parameter, which accepts values from 1 to 100." },
|
||||
{ question: "What embedding model does Sim use for knowledge base search?", answer: "Sim uses OpenAI's text-embedding-3-small model with 1536 dimensions for generating document embeddings and performing vector similarity search." },
|
||||
]} />
|
||||
|
||||
76
apps/docs/content/docs/en/mailer/index.mdx
Normal file
76
apps/docs/content/docs/en/mailer/index.mdx
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Sim Mailer
|
||||
description: Send emails to your workspace and let Sim handle them as tasks.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Sim Mailer gives your workspace a dedicated email address. Forward or send emails to it and Sim will process them as tasks — reading the subject, body, and any attachments, then replying to the thread with the result.
|
||||
|
||||
This means you can interact with Sim directly from your email client without switching apps.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Navigate to **Settings** → **Inbox**
|
||||
2. Toggle the inbox on
|
||||
3. Optionally choose a custom address prefix (e.g., `acme` → `acme@mothership.sim.ai`)
|
||||
4. Copy your inbox address and start sending emails
|
||||
|
||||
If you skip the custom prefix, one is generated automatically.
|
||||
|
||||
<Callout type="info">
|
||||
Changing your address creates a new inbox. The old address stops working immediately.
|
||||
</Callout>
|
||||
|
||||
## What You Can Send
|
||||
|
||||
Write your email like you would to a colleague. The subject and body become the task prompt.
|
||||
|
||||
**Attachments are fully supported.** Images, PDFs, and documents (up to 10 MB each) are read by Sim and displayed inline in the conversation — image attachments show as previews, just like when you upload them directly in the chat.
|
||||
|
||||
| Good email | Why it works |
|
||||
|------------|-------------|
|
||||
| "Summarize the attached PDF and list action items" | Clear task with an attachment |
|
||||
| "What's in this image?" with a photo attached | Sim reads and describes the image |
|
||||
| "Draft a reply to this forwarded thread" | Uses the email body as context |
|
||||
|
||||
## Allowed Senders
|
||||
|
||||
Only authorized senders can create tasks. Emails from anyone else are automatically rejected.
|
||||
|
||||
- **Workspace members** are allowed by default — no setup needed
|
||||
- **External senders** can be added manually with an optional label for easy identification
|
||||
|
||||
Manage your allowed senders list in **Settings** → **Inbox** → **Allowed Senders**.
|
||||
|
||||
## Tracking Tasks
|
||||
|
||||
Every email becomes a task you can track in **Settings** → **Inbox**:
|
||||
|
||||
- **Search** by subject, sender, or body content
|
||||
- **Filter** by status to find what you need
|
||||
- **Click** any completed or failed task to jump to the full conversation
|
||||
|
||||
### Task Statuses
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| **Received** | Email accepted, queued for processing |
|
||||
| **Processing** | Sim is actively working on it |
|
||||
| **Completed** | Done — the result was sent as an email reply |
|
||||
| **Failed** | Something went wrong during execution |
|
||||
| **Rejected** | Email blocked (sender not allowed, automated sender, or rate limit exceeded) |
|
||||
|
||||
## Conversations
|
||||
|
||||
Each email task creates a conversation in your workspace. You can continue the conversation from the Sim chat interface, and any follow-up emails in the same thread are linked to the same conversation.
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Can I use my own email domain?", answer: "Not currently. All inbox addresses use the @mothership.sim.ai domain. You can customize the prefix (e.g., acme@mothership.sim.ai) but not the domain itself." },
|
||||
{ question: "What happens if I send from an unauthorized email?", answer: "The email is automatically rejected. Only workspace members and manually added external senders can create tasks." },
|
||||
{ question: "Is there a size limit for attachments?", answer: "Yes, each attachment can be up to 10 MB. Images, PDFs, and common document formats are supported." },
|
||||
{ question: "Can I reply to Sim's email responses?", answer: "Yes. Replies in the same email thread are linked to the original conversation, so you can continue the interaction from your email client." },
|
||||
{ question: "How long does it take to process an email?", answer: "Most emails are processed within a few seconds. Emails with large attachments or complex tasks may take slightly longer." },
|
||||
{ question: "Can multiple people in my workspace use the same inbox?", answer: "Yes. All workspace members can send to the shared inbox address. Each email creates its own task and conversation." },
|
||||
]} />
|
||||
@@ -5,6 +5,7 @@ description: Expose your workflows as MCP tools for external AI assistants and a
|
||||
|
||||
import { Video } from '@/components/ui/video'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Deploy your workflows as MCP tools to make them accessible to external AI assistants like Claude Desktop, Cursor, and other MCP-compatible clients. This turns your workflows into callable tools that can be invoked from anywhere.
|
||||
|
||||
@@ -101,8 +102,18 @@ Workflows execute using the same deployment version as API calls, ensuring consi
|
||||
|
||||
| Action | Required Permission |
|
||||
|--------|-------------------|
|
||||
| Create MCP servers | **Admin** |
|
||||
| Create MCP servers | **Write** or **Admin** |
|
||||
| Add workflows to servers | **Write** or **Admin** |
|
||||
| View MCP servers | **Read**, **Write**, or **Admin** |
|
||||
| Delete MCP servers | **Admin** |
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Does my workflow need to be deployed before adding it as an MCP tool?", answer: "Yes. Only deployed workflows can be added as MCP tools. The MCP tool executes the same deployment version as API calls, ensuring consistent behavior between both access methods." },
|
||||
{ question: "What MCP protocol does Sim implement?", answer: "Sim implements the standard MCP protocol using the official @modelcontextprotocol/sdk types, supporting JSON-RPC 2.0 messages including tools/list and tools/call methods. It handles both requests and notifications per the MCP specification." },
|
||||
{ question: "How do I authenticate MCP client connections?", answer: "Include your API key in the X-API-Key header when connecting via mcp-remote or other HTTP-based MCP transports. The server validates authentication using hybrid auth that supports both session-based and API key-based access." },
|
||||
{ question: "Can I add the same workflow to multiple MCP servers?", answer: "Yes. When configuring a workflow as an MCP tool, you can select multiple MCP servers to add it to. Each server exposes its own URL, so you can organize tools into different servers for different use cases or clients." },
|
||||
{ question: "What naming conventions should I follow for tool names?", answer: "Use lowercase letters, numbers, and underscores only. The name should be descriptive and follow MCP naming conventions, such as search_documents or send_email. This helps AI assistants understand and correctly invoke your tools." },
|
||||
{ question: "How are workflow inputs mapped to MCP tool parameters?", answer: "Your workflow's input format fields automatically become MCP tool parameters. You can add descriptions to each parameter in the MCP configuration to help AI assistants understand what values to provide." },
|
||||
]} />
|
||||
|
||||
|
||||
|
||||
@@ -97,10 +97,11 @@ MCP functionality requires specific workspace permissions:
|
||||
|
||||
| Action | Required Permission |
|
||||
|--------|-------------------|
|
||||
| Configure MCP servers in settings | **Admin** |
|
||||
| Create or update MCP servers | **Write** or **Admin** |
|
||||
| Delete MCP servers | **Admin** |
|
||||
| Use MCP tools in agents | **Write** or **Admin** |
|
||||
| View available MCP tools | **Read**, **Write**, or **Admin** |
|
||||
| Execute MCP Tool blocks | **Write** or **Admin** |
|
||||
| Execute MCP Tool blocks | **Read**, **Write**, or **Admin** |
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
@@ -141,4 +142,15 @@ Fetch live data from external systems during workflow execution.
|
||||
### Permission Errors
|
||||
- Confirm your workspace permission level
|
||||
- Check if the MCP server requires additional authentication
|
||||
- Verify the server is properly configured for your workspace
|
||||
- Verify the server is properly configured for your workspace
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What is the difference between using MCP tools in an Agent block vs. the standalone MCP Tool block?", answer: "When you add MCP tools to an Agent block, the AI decides which tools to use based on the conversation context and its reasoning. This is best for dynamic, conversational workflows. The standalone MCP Tool block executes a specific tool with explicit parameters every time, giving you deterministic, predictable execution. Use Agent blocks for flexible reasoning and MCP Tool blocks for structured, repeatable steps." },
|
||||
{ question: "Who can configure MCP servers in a workspace?", answer: "Users with Write permission can configure (add and update) MCP servers in workspace settings. Only Admin permission is required to delete MCP servers. Users with Read permission can view available MCP tools and execute them in agents and MCP Tool blocks. This means all workspace members with at least Read access can use MCP tools in their workflows." },
|
||||
{ question: "Can I use MCP servers from multiple workspaces?", answer: "MCP servers are configured per workspace. Each workspace maintains its own set of MCP server connections. If you need the same MCP server in multiple workspaces, you need to configure it separately in each workspace's settings." },
|
||||
{ question: "How do I update MCP tool schemas after a server changes its available tools?", answer: "Click the Refresh button on the MCP server in your workspace settings. This fetches the latest tool schemas from the server and automatically updates any agent blocks that use those tools with the new parameter definitions." },
|
||||
{ question: "Can permission groups restrict access to MCP tools?", answer: "Yes. Organization admins can create permission groups that disable MCP tools for specific members using the disableMcpTools configuration option. When this is enabled, affected users will not be able to add or use MCP tools in their workflows." },
|
||||
{ question: "What happens if an MCP server goes offline during workflow execution?", answer: "If the MCP server is unreachable during execution, the tool call will fail and return an error. In an Agent block, the AI may attempt to handle the failure gracefully. In a standalone MCP Tool block, the workflow step will fail. Check MCP server logs and verify the server is running and accessible to troubleshoot connectivity issues." },
|
||||
]} />
|
||||
@@ -10,13 +10,13 @@
|
||||
"connections",
|
||||
"mcp",
|
||||
"copilot",
|
||||
"mailer",
|
||||
"skills",
|
||||
"knowledgebase",
|
||||
"variables",
|
||||
"credentials",
|
||||
"execution",
|
||||
"permissions",
|
||||
"sdks",
|
||||
"self-hosting",
|
||||
"./enterprise/index",
|
||||
"./keyboard-shortcuts/index"
|
||||
|
||||
@@ -113,7 +113,7 @@ Users can create two types of environment variables:
|
||||
### Personal Environment Variables
|
||||
- Only visible to the individual user
|
||||
- Available in all workflows they run
|
||||
- Managed in user settings
|
||||
- Managed in **Settings**, then go to **Secrets**
|
||||
|
||||
### Workspace Environment Variables
|
||||
- **Read permission**: Can see variable names and values
|
||||
@@ -154,8 +154,19 @@ When inviting someone to your organization, you can assign one of two roles:
|
||||
- Manage billing and subscription settings
|
||||
- Access all workspaces within the organization
|
||||
|
||||
### Organization Member
|
||||
### Organization Member
|
||||
**What they can do:**
|
||||
- Access workspaces they've been specifically invited to
|
||||
- View the list of organization members
|
||||
- Cannot invite new people or manage organization settings
|
||||
- Cannot invite new people or manage organization settings
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What is the difference between organization roles and workspace permissions?", answer: "Organization roles (Admin or Member) control who can manage the organization itself, including inviting people, creating workspaces, and handling billing. Workspace permissions (Read, Write, Admin) control what a user can do within a specific workspace, such as viewing, editing, or managing workflows. A user needs both an organization role and a workspace permission to work within a workspace." },
|
||||
{ question: "Can I restrict which integrations or model providers a team member can use?", answer: "Yes. Organization admins can create permission groups with fine-grained controls, including restricting allowed integrations and allowed model providers to specific lists. You can also disable access to MCP tools, custom tools, skills, and various platform features like the knowledge base, API keys, or Copilot on a per-group basis." },
|
||||
{ question: "What happens when a personal environment variable has the same name as a workspace variable?", answer: "The personal environment variable takes priority. When a workflow runs, if both a personal and workspace variable share the same name, the personal value is used. This allows individual users to override shared workspace configuration when needed." },
|
||||
{ question: "Can an Admin remove the workspace owner?", answer: "No. The workspace owner cannot be removed from the workspace by anyone. Only the workspace owner can delete the workspace or transfer ownership to another user. Admins can do everything else, including inviting and removing other users and managing workspace settings." },
|
||||
{ question: "What are permission groups and how do they work?", answer: "Permission groups are an advanced access control feature that lets organization admins define granular restrictions beyond the standard Read/Write/Admin roles. A permission group can hide UI sections (like trace spans, knowledge base, API keys, or deployment options), disable features (MCP tools, custom tools, skills, invitations), and restrict which integrations and model providers members can access. Members can be assigned to groups, and new members can be auto-added." },
|
||||
{ question: "How should I set up permissions for a new team member?", answer: "Start with the lowest permission level they need. Invite them to the organization as a Member, then add them to the relevant workspace with Read permission if they only need visibility, Write if they need to create and run workflows, or Admin if they need to manage the workspace and its users. You can always increase permissions later." },
|
||||
]} />
|
||||
@@ -65,14 +65,14 @@ Execute a workflow with optional input data.
|
||||
```python
|
||||
result = client.execute_workflow(
|
||||
"workflow-id",
|
||||
input_data={"message": "Hello, world!"},
|
||||
input={"message": "Hello, world!"},
|
||||
timeout=30.0 # 30 seconds
|
||||
)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflow_id` (str): The ID of the workflow to execute
|
||||
- `input_data` (dict, optional): Input data to pass to the workflow
|
||||
- `input` (dict, optional): Input data to pass to the workflow
|
||||
- `timeout` (float, optional): Timeout in seconds (default: 30.0)
|
||||
- `stream` (bool, optional): Enable streaming responses (default: False)
|
||||
- `selected_outputs` (list[str], optional): Block outputs to stream in `blockName.attribute` format (e.g., `["agent1.content"]`)
|
||||
@@ -144,7 +144,7 @@ Execute a workflow with automatic retry on rate limit errors using exponential b
|
||||
```python
|
||||
result = client.execute_with_retry(
|
||||
"workflow-id",
|
||||
input_data={"message": "Hello"},
|
||||
input={"message": "Hello"},
|
||||
timeout=30.0,
|
||||
max_retries=3, # Maximum number of retries
|
||||
initial_delay=1.0, # Initial delay in seconds
|
||||
@@ -155,7 +155,7 @@ result = client.execute_with_retry(
|
||||
|
||||
**Parameters:**
|
||||
- `workflow_id` (str): The ID of the workflow to execute
|
||||
- `input_data` (dict, optional): Input data to pass to the workflow
|
||||
- `input` (dict, optional): Input data to pass to the workflow
|
||||
- `timeout` (float, optional): Timeout in seconds
|
||||
- `stream` (bool, optional): Enable streaming responses
|
||||
- `selected_outputs` (list, optional): Block outputs to stream
|
||||
@@ -359,7 +359,7 @@ def run_workflow():
|
||||
# Execute the workflow
|
||||
result = client.execute_workflow(
|
||||
"my-workflow-id",
|
||||
input_data={
|
||||
input={
|
||||
"message": "Process this data",
|
||||
"user_id": "12345"
|
||||
}
|
||||
@@ -488,7 +488,7 @@ def execute_async():
|
||||
# Start async execution
|
||||
result = client.execute_workflow(
|
||||
"workflow-id",
|
||||
input_data={"data": "large dataset"},
|
||||
input={"data": "large dataset"},
|
||||
async_execution=True # Execute asynchronously
|
||||
)
|
||||
|
||||
@@ -533,7 +533,7 @@ def execute_with_retry_handling():
|
||||
# Automatically retries on rate limit
|
||||
result = client.execute_with_retry(
|
||||
"workflow-id",
|
||||
input_data={"message": "Process this"},
|
||||
input={"message": "Process this"},
|
||||
max_retries=5,
|
||||
initial_delay=1.0,
|
||||
max_delay=60.0,
|
||||
@@ -615,7 +615,7 @@ def execute_with_streaming():
|
||||
# Enable streaming for specific block outputs
|
||||
result = client.execute_workflow(
|
||||
"workflow-id",
|
||||
input_data={"message": "Count to five"},
|
||||
input={"message": "Count to five"},
|
||||
stream=True,
|
||||
selected_outputs=["agent1.content"] # Use blockName.attribute format
|
||||
)
|
||||
@@ -758,4 +758,15 @@ Configure the client using environment variables:
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
Apache-2.0
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Do I need to deploy a workflow before I can execute it via the SDK?", answer: "Yes. Workflows must be deployed before they can be executed through the SDK. You can use the validate_workflow() method to check whether a workflow is deployed and ready. If it returns False, deploy the workflow from the Sim UI first and create or select an API key during deployment." },
|
||||
{ question: "What is the difference between sync and async execution?", answer: "Sync execution (the default) blocks until the workflow completes and returns the full result. Async execution (async_execution=True) returns immediately with a task ID that you can poll using get_job_status(). Use async mode for long-running workflows to avoid request timeouts. Async job statuses include queued, processing, completed, failed, and cancelled." },
|
||||
{ question: "How does the SDK handle rate limiting?", answer: "The SDK provides built-in rate limiting support through the execute_with_retry() method. It uses exponential backoff (1s, 2s, 4s, 8s...) with 25% jitter to avoid thundering herd problems. If the API returns a retry-after header, that value is used instead. You can configure max_retries, initial_delay, max_delay, and backoff_multiplier. Use get_rate_limit_info() to check your current rate limit status." },
|
||||
{ question: "Can I use the Python SDK as a context manager?", answer: "Yes. The SimStudioClient supports Python's context manager protocol. Use it with the 'with' statement to automatically close the underlying HTTP session when you are done, which is especially useful for scripts that create and discard client instances." },
|
||||
{ question: "How do I handle different types of errors from the SDK?", answer: "The SDK raises SimStudioError with a code property for API-specific errors. Common error codes are UNAUTHORIZED (invalid API key), TIMEOUT (request timed out), RATE_LIMIT_EXCEEDED (too many requests), USAGE_LIMIT_EXCEEDED (billing limit reached), and EXECUTION_ERROR (workflow failed). Use the error code to implement targeted error handling and recovery logic." },
|
||||
{ question: "How do I monitor my API usage and remaining quota?", answer: "Use the get_usage_limits() method to check your current usage. It returns sync and async rate limit details (limit, remaining, reset time, whether you are currently limited), plus your current period cost, usage limit, and plan tier. This lets you monitor consumption and alert before hitting limits." },
|
||||
]} />
|
||||
@@ -78,16 +78,15 @@ new SimStudioClient(config: SimStudioConfig)
|
||||
Execute a workflow with optional input data.
|
||||
|
||||
```typescript
|
||||
const result = await client.executeWorkflow('workflow-id', {
|
||||
input: { message: 'Hello, world!' },
|
||||
const result = await client.executeWorkflow('workflow-id', { message: 'Hello, world!' }, {
|
||||
timeout: 30000 // 30 seconds
|
||||
});
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `workflowId` (string): The ID of the workflow to execute
|
||||
- `input` (any, optional): Input data to pass to the workflow
|
||||
- `options` (ExecutionOptions, optional):
|
||||
- `input` (any): Input data to pass to the workflow
|
||||
- `timeout` (number): Timeout in milliseconds (default: 30000)
|
||||
- `stream` (boolean): Enable streaming responses (default: false)
|
||||
- `selectedOutputs` (string[]): Block outputs to stream in `blockName.attribute` format (e.g., `["agent1.content"]`)
|
||||
@@ -158,8 +157,7 @@ if (status.status === 'completed') {
|
||||
Execute a workflow with automatic retry on rate limit errors using exponential backoff.
|
||||
|
||||
```typescript
|
||||
const result = await client.executeWithRetry('workflow-id', {
|
||||
input: { message: 'Hello' },
|
||||
const result = await client.executeWithRetry('workflow-id', { message: 'Hello' }, {
|
||||
timeout: 30000
|
||||
}, {
|
||||
maxRetries: 3, // Maximum number of retries
|
||||
@@ -171,6 +169,7 @@ const result = await client.executeWithRetry('workflow-id', {
|
||||
|
||||
**Parameters:**
|
||||
- `workflowId` (string): The ID of the workflow to execute
|
||||
- `input` (any, optional): Input data to pass to the workflow
|
||||
- `options` (ExecutionOptions, optional): Same as `executeWorkflow()`
|
||||
- `retryOptions` (RetryOptions, optional):
|
||||
- `maxRetries` (number): Maximum number of retries (default: 3)
|
||||
@@ -389,10 +388,8 @@ async function runWorkflow() {
|
||||
|
||||
// Execute the workflow
|
||||
const result = await client.executeWorkflow('my-workflow-id', {
|
||||
input: {
|
||||
message: 'Process this data',
|
||||
userId: '12345'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
@@ -508,8 +505,7 @@ app.post('/execute-workflow', async (req, res) => {
|
||||
try {
|
||||
const { workflowId, input } = req.body;
|
||||
|
||||
const result = await client.executeWorkflow(workflowId, {
|
||||
input,
|
||||
const result = await client.executeWorkflow(workflowId, input, {
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
@@ -555,8 +551,7 @@ export default async function handler(
|
||||
try {
|
||||
const { workflowId, input } = req.body;
|
||||
|
||||
const result = await client.executeWorkflow(workflowId, {
|
||||
input,
|
||||
const result = await client.executeWorkflow(workflowId, input, {
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
@@ -586,9 +581,7 @@ const client = new SimStudioClient({
|
||||
async function executeClientSideWorkflow() {
|
||||
try {
|
||||
const result = await client.executeWorkflow('workflow-id', {
|
||||
input: {
|
||||
userInput: 'Hello from browser'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Workflow result:', result);
|
||||
@@ -642,10 +635,8 @@ Alternatively, you can manually provide files using the URL format:
|
||||
|
||||
// Include files under the field name from your API trigger's input format
|
||||
const result = await client.executeWorkflow('workflow-id', {
|
||||
input: {
|
||||
documents: files, // Must match your workflow's "files" field name
|
||||
instructions: 'Analyze these documents'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Result:', result);
|
||||
@@ -669,10 +660,8 @@ Alternatively, you can manually provide files using the URL format:
|
||||
|
||||
// Include files under the field name from your API trigger's input format
|
||||
const result = await client.executeWorkflow('workflow-id', {
|
||||
input: {
|
||||
documents: [file], // Must match your workflow's "files" field name
|
||||
query: 'Summarize this document'
|
||||
}
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
@@ -712,8 +701,7 @@ export function useWorkflow(): UseWorkflowResult {
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const workflowResult = await client.executeWorkflow(workflowId, {
|
||||
input,
|
||||
const workflowResult = await client.executeWorkflow(workflowId, input, {
|
||||
timeout: 30000
|
||||
});
|
||||
setResult(workflowResult);
|
||||
@@ -774,8 +762,7 @@ const client = new SimStudioClient({
|
||||
async function executeAsync() {
|
||||
try {
|
||||
// Start async execution
|
||||
const result = await client.executeWorkflow('workflow-id', {
|
||||
input: { data: 'large dataset' },
|
||||
const result = await client.executeWorkflow('workflow-id', { data: 'large dataset' }, {
|
||||
async: true // Execute asynchronously
|
||||
});
|
||||
|
||||
@@ -823,9 +810,7 @@ const client = new SimStudioClient({
|
||||
async function executeWithRetryHandling() {
|
||||
try {
|
||||
// Automatically retries on rate limit
|
||||
const result = await client.executeWithRetry('workflow-id', {
|
||||
input: { message: 'Process this' }
|
||||
}, {
|
||||
const result = await client.executeWithRetry('workflow-id', { message: 'Process this' }, {}, {
|
||||
maxRetries: 5,
|
||||
initialDelay: 1000,
|
||||
maxDelay: 60000,
|
||||
@@ -908,8 +893,7 @@ const client = new SimStudioClient({
|
||||
async function executeWithStreaming() {
|
||||
try {
|
||||
// Enable streaming for specific block outputs
|
||||
const result = await client.executeWorkflow('workflow-id', {
|
||||
input: { message: 'Count to five' },
|
||||
const result = await client.executeWorkflow('workflow-id', { message: 'Count to five' }, {
|
||||
stream: true,
|
||||
selectedOutputs: ['agent1.content'] // Use blockName.attribute format
|
||||
});
|
||||
@@ -1033,3 +1017,14 @@ function StreamingWorkflow() {
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Do I need to deploy a workflow before I can execute it via the SDK?", answer: "Yes. Workflows must be deployed before they can be executed through the SDK. You can use the validateWorkflow() method to check whether a workflow is deployed and ready. If it returns false, deploy the workflow from the Sim UI first and create or select an API key during deployment." },
|
||||
{ question: "What is the difference between sync and async execution?", answer: "Sync execution (the default) blocks until the workflow completes and returns the full result. Async execution returns immediately with a task ID that you can poll using getJobStatus(). Use async mode for long-running workflows to avoid request timeouts. Async job statuses include queued, processing, completed, failed, and cancelled." },
|
||||
{ question: "How does streaming work with the SDK?", answer: "Enable streaming by setting stream: true and specifying selectedOutputs with block names and attributes in blockName.attribute format (e.g., ['agent1.content']). The response uses Server-Sent Events (SSE) format, sending incremental chunks as the workflow executes. Each chunk includes the blockId and the text content. A final done event includes the execution metadata." },
|
||||
{ question: "How does the SDK handle rate limiting?", answer: "The SDK provides built-in rate limiting support through the executeWithRetry() method. It uses exponential backoff (1s, 2s, 4s, 8s...) with 25% jitter to avoid thundering herd problems. If the API returns a retry-after header, that value is used instead. You can configure maxRetries, initialDelay, maxDelay, and backoffMultiplier. Use getRateLimitInfo() to check your current rate limit status." },
|
||||
{ question: "Is it safe to use the SDK in browser-side code?", answer: "You can use the SDK in the browser, but you should not expose your API key in client-side code. In production, use a backend proxy server to handle SDK calls, or use a public API key with limited permissions. The SDK works with both Node.js and browser environments, but sensitive keys should stay server-side." },
|
||||
{ question: "How do I send files to a workflow through the SDK?", answer: "File objects are automatically detected and converted to base64 format. Include them in the input object under the field name that matches your workflow's API trigger input format. In the browser, pass File objects directly from file inputs. In Node.js, create File objects from buffers. You can also provide files as URL references with type, data, name, and mime fields." },
|
||||
]} />
|
||||
|
||||
@@ -5,6 +5,7 @@ description: Deploy Sim with Docker Compose
|
||||
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -148,3 +149,14 @@ docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compo
|
||||
# Backup database
|
||||
docker compose -f docker-compose.prod.yml exec db pg_dump -U postgres simstudio > backup.sql
|
||||
```
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What containers are started by docker-compose.prod.yml?", answer: "Four services are started: simstudio (main app on port 3000, 8 GB memory limit), realtime (WebSocket server on port 3002, 1 GB memory limit), db (PostgreSQL 17 with pgvector on port 5432), and migrations (runs once to apply database schema changes, then exits)." },
|
||||
{ question: "How do I configure SSL for production?", answer: "You can use either Caddy (recommended, handles certificates automatically) or Nginx with Certbot. Both need to reverse-proxy port 3000 for the main app and port 3002 for WebSocket connections at the /socket.io/ path." },
|
||||
{ question: "Why can I not connect to Ollama running on my host from inside Docker?", answer: "Inside a Docker container, localhost refers to the container itself, not your host machine. On macOS and Windows, use http://host.docker.internal:11434. On Linux, use your host machine's actual IP address (e.g., http://192.168.1.100:11434)." },
|
||||
{ question: "What is the difference between the GPU and CPU Ollama profiles?", answer: "The GPU profile (--profile gpu) configures NVIDIA driver capabilities and reserves GPU devices for accelerated inference. The CPU profile (--profile cpu) runs Ollama without GPU acceleration. Both use the --profile setup flag to automatically pull the gemma3:4b starter model." },
|
||||
{ question: "How do I update Sim to the latest version?", answer: "Run docker compose -f docker-compose.prod.yml pull to fetch the latest images, then docker compose -f docker-compose.prod.yml up -d to restart with the new versions. The migrations container will automatically apply any new database schema changes on startup." },
|
||||
{ question: "How do I back up and restore the database?", answer: "Back up with: docker compose -f docker-compose.prod.yml exec db pg_dump -U postgres simstudio > backup.sql. Restore with: docker compose -f docker-compose.prod.yml exec -T db psql -U postgres simstudio < backup.sql. The database data is persisted in a Docker volume named postgres_data." },
|
||||
{ question: "Can I customize the PostgreSQL credentials?", answer: "Yes. The docker-compose.prod.yml uses environment variable defaults: POSTGRES_USER (default: postgres), POSTGRES_PASSWORD (default: postgres), POSTGRES_DB (default: simstudio), and POSTGRES_PORT (default: 5432). Set these in your .env file to override them." },
|
||||
]} />
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ description: Deploy Sim on your own infrastructure
|
||||
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
Deploy Sim on your own infrastructure with Docker or Kubernetes.
|
||||
|
||||
@@ -63,3 +64,13 @@ Open [http://localhost:3000](http://localhost:3000)
|
||||
| db | 5432 | PostgreSQL with pgvector |
|
||||
| migrations | - | Database migrations (runs once) |
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What are the minimum requirements to self-host Sim?", answer: "At minimum you need 2 CPU cores, 12 GB RAM, 20 GB SSD storage, and Docker 20.10 or later. Memory is typically the constraining factor due to workflow execution (isolated-vm sandboxing), file processing, and vector operations (pgvector)." },
|
||||
{ question: "Which database does Sim use?", answer: "Sim uses PostgreSQL 17 with the pgvector extension for vector similarity search. The Docker setup uses the pgvector/pgvector:pg17 image, which comes with the vector extension pre-installed." },
|
||||
{ question: "What are the required environment variables for production?", answer: "Three secrets are required: BETTER_AUTH_SECRET (authentication), ENCRYPTION_KEY (data encryption), and INTERNAL_API_SECRET (service-to-service auth). Generate each with openssl rand -hex 32. You also need to set NEXT_PUBLIC_APP_URL and BETTER_AUTH_URL to your domain." },
|
||||
{ question: "What does the realtime service do?", answer: "The realtime service is a dedicated WebSocket server that runs on port 3002. It handles real-time communication for features like live workflow execution updates. It has a 1 GB memory limit and requires its own DATABASE_URL and BETTER_AUTH_SECRET configuration." },
|
||||
{ question: "How does the migrations service work?", answer: "The migrations container runs once at startup (restart: no) and executes bun run db:migrate to apply database schema changes. It depends on the database being healthy before running and must complete successfully before the main application starts." },
|
||||
{ question: "Can I use Sim with local AI models?", answer: "Yes. Sim supports Ollama for local model inference. Use docker-compose.ollama.yml instead of docker-compose.prod.yml. It offers both GPU (with NVIDIA support) and CPU-only profiles, and automatically pulls gemma3:4b as a starter model." },
|
||||
]} />
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ description: Deploy Sim with Helm
|
||||
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -125,3 +126,13 @@ helm upgrade sim ./helm/sim --namespace simstudio
|
||||
# Uninstall
|
||||
helm uninstall sim --namespace simstudio
|
||||
```
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "What are the default resource limits for the Sim app pod?", answer: "The Helm chart defaults to 8 Gi memory limit / 4 Gi request and 2000m CPU limit / 1000m request per app pod. The pod runs as non-root (UID 1001) with fsGroup 1001 for security." },
|
||||
{ question: "Can I use an external database instead of the bundled PostgreSQL?", answer: "Yes. Set postgresql.enabled to false and configure the externalDatabase section with your host, port, username, password, database name, and sslMode. The external database must have the pgvector extension installed." },
|
||||
{ question: "How do I manage secrets securely in Kubernetes?", answer: "The Helm chart supports pre-existing Kubernetes secrets via app.secrets.existingSecret. Set enabled to true and provide the secret name. This integrates with External Secrets Operator, HashiCorp Vault, Azure Key Vault, and similar tools. Key mappings can be customized if your secret uses different key names." },
|
||||
{ question: "Can I scale the app to multiple replicas?", answer: "Yes. Set app.replicaCount in your values.yaml. The chart supports standard Kubernetes scaling. Ensure your database can handle the additional connections from multiple replicas." },
|
||||
{ question: "Are there cloud-specific example configurations?", answer: "Yes. The Helm chart includes example values files for AWS EKS (values-aws.yaml), Azure AKS (values-azure.yaml), GCP GKE (values-gcp.yaml), as well as files for production, development, external databases, external secrets, and whitelabeled deployments." },
|
||||
{ question: "What ingress options are supported?", answer: "The chart supports ingress with configurable className (e.g., nginx), TLS termination, and separate host configuration for the app. Enable it with ingress.enabled: true and set your host under ingress.app.host." },
|
||||
]} />
|
||||
|
||||
|
||||
@@ -132,3 +132,15 @@ Skills are most valuable when agents need specialized knowledge or multi-step wo
|
||||
- [Agent Skills specification](https://agentskills.io) — The open format for portable agent skills
|
||||
- [Example skills](https://github.com/anthropics/skills) — Browse community skill examples
|
||||
- [Best practices](https://agentskills.io/what-are-skills) — Writing effective skills
|
||||
|
||||
import { FAQ } from '@/components/ui/faq'
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "How many skills can I attach to a single agent?", answer: "You can attach as many skills as you want, but the recommended limit is 5-10 per agent. More skills mean more decision overhead for the agent when scanning descriptions. Since only the names and descriptions are included in the system prompt (about 50-100 tokens each), many skills will not dramatically increase context usage, but they can slow down the agent's decision-making." },
|
||||
{ question: "How does the agent decide when to load a skill?", answer: "The agent sees an available_skills section in its system prompt listing each skill's name and description. When the agent determines that a skill is relevant to the current task, it calls the load_skill tool with the skill name. The full skill content is then returned as a tool response. This is why writing a specific, keyword-rich description is critical -- it is the only thing the agent reads before deciding whether to activate a skill." },
|
||||
{ question: "Do skills work with all LLM providers?", answer: "Yes. The load_skill mechanism uses standard tool-calling, which is supported by all LLM providers in Sim. No provider-specific configuration is needed. The skill system works the same way whether you are using Anthropic, OpenAI, Google, or any other supported provider." },
|
||||
{ question: "When should I use skills vs. agent instructions?", answer: "Use skills for knowledge that applies across multiple workflows or changes frequently. Skills are reusable packages that can be attached to any agent. Use agent instructions for task-specific context that is unique to a single agent and workflow. If you find yourself copying the same instructions into multiple agents, that content should be a skill instead." },
|
||||
{ question: "Can permission groups disable skills for certain users?", answer: "Yes. Organization admins can create permission groups with the disableSkills option enabled. When a user is assigned to such a permission group, the skills dropdown in agent blocks will be disabled and they will not be able to add or use skills in their workflows." },
|
||||
{ question: "What is the recommended maximum length for skill content?", answer: "Keep skills focused and under 500 lines. If a skill grows too large, split it into multiple specialized skills. Shorter, focused skills are more effective because the agent can load exactly what it needs. A broad skill with too much content can overwhelm the agent and reduce the quality of its responses." },
|
||||
{ question: "Where do I create and manage skills?", answer: "Go to Settings and select Skills under the Tools section. From there you can add new skills with a name (kebab-case identifier, max 64 characters), description (max 1024 characters), and content (full instructions in markdown). You can also edit or delete existing skills from this page." },
|
||||
]} />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user