Compare commits
153 Commits
feat/agent
...
fix/linear
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ace87791d8 | ||
|
|
74af452175 | ||
|
|
ec51f73596 | ||
|
|
6866da590c | ||
|
|
b0c0ee29a8 | ||
|
|
20c05644ab | ||
|
|
f9d73db65c | ||
|
|
e2e53aba76 | ||
|
|
727bb1cadb | ||
|
|
e2e29cefd7 | ||
|
|
45f053a383 | ||
|
|
225d5d551a | ||
|
|
a78f3f9c2e | ||
|
|
080a0a6123 | ||
|
|
fc6fe193fa | ||
|
|
bbc704fe05 | ||
|
|
c016537564 | ||
|
|
4c94f3cf78 | ||
|
|
27a11a269d | ||
|
|
2c174ca4f6 | ||
|
|
ac831b85b2 | ||
|
|
8527ae5d3b | ||
|
|
076c835ba2 | ||
|
|
df6ceb61a4 | ||
|
|
2ede12aa0e | ||
|
|
42fb434354 | ||
|
|
dcebe3ae97 | ||
|
|
e39c534ee3 | ||
|
|
b95a0491a0 | ||
|
|
a79c8a75ce | ||
|
|
282ec8c58c | ||
|
|
e45fbe0184 | ||
|
|
512558dcb3 | ||
|
|
35411e465e | ||
|
|
72e28baa07 | ||
|
|
d99dd86bf2 | ||
|
|
7898e5d75f | ||
|
|
df62502903 | ||
|
|
1a2aa6949e | ||
|
|
4544fd4519 | ||
|
|
019630bdc8 | ||
|
|
90f592797a | ||
|
|
d091441e39 | ||
|
|
7d4dd26760 | ||
|
|
0abeac77e1 | ||
|
|
e9c94fa462 | ||
|
|
72eea64bf6 | ||
|
|
27460f847c | ||
|
|
c7643198dc | ||
|
|
e5aef6184a | ||
|
|
4ae5b1b620 | ||
|
|
5c334874eb | ||
|
|
d3d58a9615 | ||
|
|
1d59eca90a | ||
|
|
e1359b09d6 | ||
|
|
35b3646330 | ||
|
|
5c47ea58f8 | ||
|
|
c4f4e6b48c | ||
|
|
1728c370de | ||
|
|
82e58a5082 | ||
|
|
336c065234 | ||
|
|
b3713642b2 | ||
|
|
b9b930bb63 | ||
|
|
f1ead2ed55 | ||
|
|
30377d775b | ||
|
|
d013132d0e | ||
|
|
7b0ce8064a | ||
|
|
0ea73263df | ||
|
|
edc502384b | ||
|
|
e2be99263c | ||
|
|
f6b461ad47 | ||
|
|
e4d35735b1 | ||
|
|
b4064c57fb | ||
|
|
eac41ca105 | ||
|
|
d2c3c1c39e | ||
|
|
8f3e864751 | ||
|
|
23c3072784 | ||
|
|
33fdb11396 | ||
|
|
21156dd54a | ||
|
|
c05e2e0fc8 | ||
|
|
a7c1e510e6 | ||
|
|
271624a402 | ||
|
|
dda012eae9 | ||
|
|
2dd6d3d1e6 | ||
|
|
b90bb75cda | ||
|
|
fb233d003d | ||
|
|
34df3333d1 | ||
|
|
23677d41a0 | ||
|
|
a489f91085 | ||
|
|
ed6e7845cc | ||
|
|
e698f9fe14 | ||
|
|
db1798267e | ||
|
|
5f1d5e0618 | ||
|
|
ed5645166e | ||
|
|
50e42c2041 | ||
|
|
e70e1ec8c5 | ||
|
|
4c474e03c1 | ||
|
|
b0980b1e09 | ||
|
|
66ce673629 | ||
|
|
f37e4b67c7 | ||
|
|
7a1a46067d | ||
|
|
bf60670c0b | ||
|
|
8a481b612d | ||
|
|
bc4b7f5759 | ||
|
|
5aa0b4d5d4 | ||
|
|
3774f33d39 | ||
|
|
3597eacdb7 | ||
|
|
c5fe92567a | ||
|
|
36bc57f0b9 | ||
|
|
2771b679cb | ||
|
|
6610c37e10 | ||
|
|
a9fc1a24a9 | ||
|
|
d97e22e395 | ||
|
|
9603fd091b | ||
|
|
9e4fc5024f | ||
|
|
1a14f4c13d | ||
|
|
7583c8fbf4 | ||
|
|
7b96b0e8e8 | ||
|
|
794d5eab5e | ||
|
|
5a5c33d326 | ||
|
|
104ad03004 | ||
|
|
9d1b9763c5 | ||
|
|
be6b00d95f | ||
|
|
438defceb0 | ||
|
|
87e8d3caf8 | ||
|
|
f94be08950 | ||
|
|
54a862d5b0 | ||
|
|
e0f2b8fe58 | ||
|
|
2691c12747 | ||
|
|
8caaf01371 | ||
|
|
668b948f0b | ||
|
|
8800f03fa3 | ||
|
|
b497033795 | ||
|
|
666dc67aa2 | ||
|
|
7af7a225f2 | ||
|
|
228578e282 | ||
|
|
be647469ac | ||
|
|
96b171cf74 | ||
|
|
cdea2404e3 | ||
|
|
f6975fc0a3 | ||
|
|
59182d5db2 | ||
|
|
b9926df8e0 | ||
|
|
77eafabb63 | ||
|
|
34ea99e99d | ||
|
|
a7f344bca1 | ||
|
|
7b6149dc23 | ||
|
|
b09a073c29 | ||
|
|
8d93c850ba | ||
|
|
83eb3ed211 | ||
|
|
a783b9d4ce | ||
|
|
0c80438ede | ||
|
|
41a7d247ea | ||
|
|
092525e8aa |
825
.agents/skills/add-block/SKILL.md
Normal file
@@ -0,0 +1,825 @@
|
||||
---
|
||||
name: add-block
|
||||
description: Create or update a Sim integration block with correct subBlocks, conditions, dependsOn, modes, canonicalParamId usage, outputs, and tool wiring. Use when working on `apps/sim/blocks/blocks/{service}.ts` or aligning a block with its tools.
|
||||
---
|
||||
|
||||
# Add Block Skill
|
||||
|
||||
You are an expert at creating block configurations for Sim. You understand the serializer, subBlock types, conditions, dependsOn, modes, and all UI patterns.
|
||||
|
||||
## Your Task
|
||||
|
||||
When the user asks you to create a block:
|
||||
1. Create the block file in `apps/sim/blocks/blocks/{service}.ts`
|
||||
2. Configure all subBlocks with proper types, conditions, and dependencies
|
||||
3. Wire up tools correctly
|
||||
|
||||
## Block Configuration Structure
|
||||
|
||||
```typescript
|
||||
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
|
||||
name: '{Service Name}', // Human readable
|
||||
description: 'Brief description', // One sentence
|
||||
longDescription: 'Detailed description for docs',
|
||||
docsLink: 'https://docs.sim.ai/tools/{service}',
|
||||
category: 'tools', // 'tools' | 'blocks' | 'triggers'
|
||||
bgColor: '#HEXCOLOR', // Brand color
|
||||
icon: {ServiceName}Icon,
|
||||
|
||||
// Auth mode
|
||||
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
|
||||
|
||||
subBlocks: [
|
||||
// Define all UI fields here
|
||||
],
|
||||
|
||||
tools: {
|
||||
access: ['tool_id_1', 'tool_id_2'], // Array of tool IDs this block can use
|
||||
config: {
|
||||
tool: (params) => `{service}_${params.operation}`, // Tool selector function
|
||||
params: (params) => ({
|
||||
// Transform subBlock values to tool params
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
inputs: {
|
||||
// Optional: define expected inputs from other blocks
|
||||
},
|
||||
|
||||
outputs: {
|
||||
// Define outputs available to downstream blocks
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## SubBlock Types Reference
|
||||
|
||||
**Critical:** Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions.
|
||||
|
||||
### Text Inputs
|
||||
```typescript
|
||||
// Single-line input
|
||||
{ id: 'field', title: 'Label', type: 'short-input', placeholder: '...' }
|
||||
|
||||
// Multi-line input
|
||||
{ id: 'field', title: 'Label', type: 'long-input', placeholder: '...', rows: 6 }
|
||||
|
||||
// Password input
|
||||
{ id: 'apiKey', title: 'API Key', type: 'short-input', password: true }
|
||||
```
|
||||
|
||||
### Selection Inputs
|
||||
```typescript
|
||||
// Dropdown (static options)
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Create', id: 'create' },
|
||||
{ label: 'Update', id: 'update' },
|
||||
],
|
||||
value: () => 'create', // Default value function
|
||||
}
|
||||
|
||||
// Combobox (searchable dropdown)
|
||||
{
|
||||
id: 'field',
|
||||
title: 'Label',
|
||||
type: 'combobox',
|
||||
options: [...],
|
||||
searchable: true,
|
||||
}
|
||||
```
|
||||
|
||||
### Code/JSON Inputs
|
||||
```typescript
|
||||
{
|
||||
id: 'code',
|
||||
title: 'Code',
|
||||
type: 'code',
|
||||
language: 'javascript', // 'javascript' | 'json' | 'python'
|
||||
placeholder: '// Enter code...',
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth/Credentials
|
||||
```typescript
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Account',
|
||||
type: 'oauth-input',
|
||||
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.)
|
||||
{
|
||||
id: 'channel',
|
||||
title: 'Channel',
|
||||
type: 'channel-selector',
|
||||
serviceId: '{service}',
|
||||
placeholder: 'Select channel',
|
||||
dependsOn: ['credential'],
|
||||
}
|
||||
|
||||
// Project selector (Jira, etc.)
|
||||
{
|
||||
id: 'project',
|
||||
title: 'Project',
|
||||
type: 'project-selector',
|
||||
serviceId: '{service}',
|
||||
dependsOn: ['credential'],
|
||||
}
|
||||
|
||||
// File selector (Google Drive, etc.)
|
||||
{
|
||||
id: 'file',
|
||||
title: 'File',
|
||||
type: 'file-selector',
|
||||
serviceId: '{service}',
|
||||
mimeType: 'application/pdf',
|
||||
dependsOn: ['credential'],
|
||||
}
|
||||
|
||||
// User selector
|
||||
{
|
||||
id: 'user',
|
||||
title: 'User',
|
||||
type: 'user-selector',
|
||||
serviceId: '{service}',
|
||||
dependsOn: ['credential'],
|
||||
}
|
||||
```
|
||||
|
||||
### Other Types
|
||||
```typescript
|
||||
// Switch/toggle
|
||||
{ id: 'enabled', type: 'switch' }
|
||||
|
||||
// Slider
|
||||
{ id: 'temperature', title: 'Temperature', type: 'slider', min: 0, max: 2, step: 0.1 }
|
||||
|
||||
// Table (key-value pairs)
|
||||
{ id: 'headers', title: 'Headers', type: 'table', columns: ['Key', 'Value'] }
|
||||
|
||||
// File upload
|
||||
{
|
||||
id: 'files',
|
||||
title: 'Attachments',
|
||||
type: 'file-upload',
|
||||
multiple: true,
|
||||
acceptedTypes: 'image/*,application/pdf',
|
||||
}
|
||||
```
|
||||
|
||||
## File Input Handling
|
||||
|
||||
When your block accepts file uploads, use the basic/advanced mode pattern with `normalizeFileInput`.
|
||||
|
||||
### Basic/Advanced File Pattern
|
||||
|
||||
```typescript
|
||||
// Basic mode: Visual file upload
|
||||
{
|
||||
id: 'uploadFile',
|
||||
title: 'File',
|
||||
type: 'file-upload',
|
||||
canonicalParamId: 'file', // Both map to 'file' param
|
||||
placeholder: 'Upload file',
|
||||
mode: 'basic',
|
||||
multiple: false,
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'upload' },
|
||||
},
|
||||
// Advanced mode: Reference from other blocks
|
||||
{
|
||||
id: 'fileRef',
|
||||
title: 'File',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'file', // Both map to 'file' param
|
||||
placeholder: 'Reference file (e.g., {{file_block.output}})',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'upload' },
|
||||
},
|
||||
```
|
||||
|
||||
**Critical constraints:**
|
||||
- `canonicalParamId` must NOT match any subblock's `id` in the same block
|
||||
- Values are stored under subblock `id`, not `canonicalParamId`
|
||||
|
||||
### Normalizing File Input in tools.config
|
||||
|
||||
Use `normalizeFileInput` to handle all input variants:
|
||||
|
||||
```typescript
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
|
||||
tools: {
|
||||
access: ['service_upload'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Check all field IDs: uploadFile (basic), fileRef (advanced), fileContent (legacy)
|
||||
const normalizedFile = normalizeFileInput(
|
||||
params.uploadFile || params.fileRef || params.fileContent,
|
||||
{ single: true }
|
||||
)
|
||||
if (normalizedFile) {
|
||||
params.file = normalizedFile
|
||||
}
|
||||
return `service_${params.operation}`
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Why this pattern?**
|
||||
- Values come through as `params.uploadFile` or `params.fileRef` (the subblock IDs)
|
||||
- `canonicalParamId` only controls UI/schema mapping, not runtime values
|
||||
- `normalizeFileInput` handles JSON strings from advanced mode template resolution
|
||||
|
||||
### File Input Types in `inputs`
|
||||
|
||||
Use `type: 'json'` for file inputs:
|
||||
|
||||
```typescript
|
||||
inputs: {
|
||||
uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' },
|
||||
fileRef: { type: 'json', description: 'File reference from previous block' },
|
||||
// Legacy field for backwards compatibility
|
||||
fileContent: { type: 'string', description: 'Legacy: base64 encoded content' },
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Files
|
||||
|
||||
For multiple file uploads:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'attachments',
|
||||
title: 'Attachments',
|
||||
type: 'file-upload',
|
||||
multiple: true, // Allow multiple files
|
||||
maxSize: 25, // Max size in MB per file
|
||||
acceptedTypes: 'image/*,application/pdf,.doc,.docx',
|
||||
}
|
||||
|
||||
// In tools.config:
|
||||
const normalizedFiles = normalizeFileInput(
|
||||
params.attachments || params.attachmentRefs,
|
||||
// No { single: true } - returns array
|
||||
)
|
||||
if (normalizedFiles) {
|
||||
params.files = normalizedFiles
|
||||
}
|
||||
```
|
||||
|
||||
## Condition Syntax
|
||||
|
||||
Controls when a field is shown based on other field values.
|
||||
|
||||
### Simple Condition
|
||||
```typescript
|
||||
condition: { field: 'operation', value: 'create' }
|
||||
// Shows when operation === 'create'
|
||||
```
|
||||
|
||||
### Multiple Values (OR)
|
||||
```typescript
|
||||
condition: { field: 'operation', value: ['create', 'update'] }
|
||||
// Shows when operation is 'create' OR 'update'
|
||||
```
|
||||
|
||||
### Negation
|
||||
```typescript
|
||||
condition: { field: 'operation', value: 'delete', not: true }
|
||||
// Shows when operation !== 'delete'
|
||||
```
|
||||
|
||||
### Compound (AND)
|
||||
```typescript
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'send',
|
||||
and: {
|
||||
field: 'type',
|
||||
value: 'dm',
|
||||
not: true,
|
||||
}
|
||||
}
|
||||
// Shows when operation === 'send' AND type !== 'dm'
|
||||
```
|
||||
|
||||
### Complex Example
|
||||
```typescript
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list', 'search'],
|
||||
not: true,
|
||||
and: {
|
||||
field: 'authMethod',
|
||||
value: 'oauth',
|
||||
}
|
||||
}
|
||||
// Shows when operation NOT in ['list', 'search'] AND authMethod === 'oauth'
|
||||
```
|
||||
|
||||
## DependsOn Pattern
|
||||
|
||||
Controls when a field is enabled and when its options are refetched.
|
||||
|
||||
### Simple Array (all must be set)
|
||||
```typescript
|
||||
dependsOn: ['credential']
|
||||
// Enabled only when credential has a value
|
||||
// Options refetch when credential changes
|
||||
|
||||
dependsOn: ['credential', 'projectId']
|
||||
// Enabled only when BOTH have values
|
||||
```
|
||||
|
||||
### Complex (all + any)
|
||||
```typescript
|
||||
dependsOn: {
|
||||
all: ['authMethod'], // All must be set
|
||||
any: ['credential', 'apiKey'] // At least one must be set
|
||||
}
|
||||
// Enabled when authMethod is set AND (credential OR apiKey is set)
|
||||
```
|
||||
|
||||
## Required Pattern
|
||||
|
||||
Can be boolean or condition-based.
|
||||
|
||||
### Simple Boolean
|
||||
```typescript
|
||||
required: true
|
||||
required: false
|
||||
```
|
||||
|
||||
### Conditional Required
|
||||
```typescript
|
||||
required: { field: 'operation', value: 'create' }
|
||||
// Required only when operation === 'create'
|
||||
|
||||
required: { field: 'operation', value: ['create', 'update'] }
|
||||
// Required when operation is 'create' OR 'update'
|
||||
```
|
||||
|
||||
## Mode Pattern (Basic vs Advanced)
|
||||
|
||||
Controls which UI view shows the field.
|
||||
|
||||
### Mode Options
|
||||
- `'basic'` - Only in basic view (default UI)
|
||||
- `'advanced'` - Only in advanced view
|
||||
- `'both'` - Both views (default if not specified)
|
||||
- `'trigger'` - Only in trigger configuration
|
||||
|
||||
### canonicalParamId Pattern
|
||||
|
||||
Maps multiple UI fields to a single serialized parameter:
|
||||
|
||||
```typescript
|
||||
// Basic mode: Visual selector
|
||||
{
|
||||
id: 'channel',
|
||||
title: 'Channel',
|
||||
type: 'channel-selector',
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'channel', // Both map to 'channel' param
|
||||
dependsOn: ['credential'],
|
||||
}
|
||||
|
||||
// Advanced mode: Manual input
|
||||
{
|
||||
id: 'channelId',
|
||||
title: 'Channel ID',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
canonicalParamId: 'channel', // Both map to 'channel' param
|
||||
placeholder: 'Enter channel ID manually',
|
||||
}
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- In basic mode: `channel` selector value → `params.channel`
|
||||
- In advanced mode: `channelId` input value → `params.channel`
|
||||
- The serializer consolidates based on current mode
|
||||
|
||||
**Critical constraints:**
|
||||
- `canonicalParamId` must NOT match any other subblock's `id` in the same block (causes conflicts)
|
||||
- `canonicalParamId` must be unique per block (only one basic/advanced pair per canonicalParamId)
|
||||
- ONLY use `canonicalParamId` to link basic/advanced mode alternatives for the same logical parameter
|
||||
- Do NOT use it for any other purpose
|
||||
|
||||
## WandConfig Pattern
|
||||
|
||||
Enables AI-assisted field generation.
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'query',
|
||||
title: 'Query',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate a query based on the user request. Return ONLY the JSON.',
|
||||
placeholder: 'Describe what you want to query...',
|
||||
generationType: 'json-object', // Optional: affects AI behavior
|
||||
maintainHistory: true, // Optional: keeps conversation context
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Generation Types
|
||||
- `'javascript-function-body'` - JS code generation
|
||||
- `'json-object'` - Raw JSON (adds "no markdown" instruction)
|
||||
- `'json-schema'` - JSON Schema definitions
|
||||
- `'sql-query'` - SQL statements
|
||||
- `'timestamp'` - Adds current date/time context
|
||||
|
||||
## Tools Configuration
|
||||
|
||||
**Important:** `tools.config.tool` runs during serialization before variable resolution. Put `Number()` and other type coercions in `tools.config.params` instead, which runs at execution time after variables are resolved.
|
||||
|
||||
**Preferred:** Use tool names directly as dropdown option IDs to avoid switch cases:
|
||||
```typescript
|
||||
// Dropdown options use tool IDs directly
|
||||
options: [
|
||||
{ label: 'Create', id: 'service_create' },
|
||||
{ label: 'Read', id: 'service_read' },
|
||||
]
|
||||
|
||||
// Tool selector just returns the operation value
|
||||
tool: (params) => params.operation,
|
||||
```
|
||||
|
||||
### With Parameter Transformation
|
||||
```typescript
|
||||
tools: {
|
||||
access: ['service_action'],
|
||||
config: {
|
||||
tool: (params) => 'service_action',
|
||||
params: (params) => ({
|
||||
id: params.resourceId,
|
||||
data: typeof params.data === 'string' ? JSON.parse(params.data) : params.data,
|
||||
}),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### V2 Versioned Tool Selector
|
||||
```typescript
|
||||
import { createVersionedToolSelector } from '@/blocks/utils'
|
||||
|
||||
tools: {
|
||||
access: [
|
||||
'service_create_v2',
|
||||
'service_read_v2',
|
||||
'service_update_v2',
|
||||
],
|
||||
config: {
|
||||
tool: createVersionedToolSelector({
|
||||
baseToolSelector: (params) => `service_${params.operation}`,
|
||||
suffix: '_v2',
|
||||
fallbackToolId: 'service_create_v2',
|
||||
}),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Outputs Definition
|
||||
|
||||
**IMPORTANT:** Block outputs have a simpler schema than tool outputs. Block outputs do NOT support:
|
||||
- `optional: true` - This is only for tool outputs
|
||||
- `items` property - This is only for tool outputs with array types
|
||||
|
||||
Block outputs only support:
|
||||
- `type` - The data type ('string', 'number', 'boolean', 'json', 'array')
|
||||
- `description` - Human readable description
|
||||
- Nested object structure (for complex types)
|
||||
|
||||
```typescript
|
||||
outputs: {
|
||||
// Simple outputs
|
||||
id: { type: 'string', description: 'Resource ID' },
|
||||
success: { type: 'boolean', description: 'Whether operation succeeded' },
|
||||
|
||||
// Use type: 'json' for complex objects or arrays (NOT type: 'array' with items)
|
||||
items: { type: 'json', description: 'List of items' },
|
||||
metadata: { type: 'json', description: 'Response metadata' },
|
||||
|
||||
// Nested outputs (for structured data)
|
||||
user: {
|
||||
id: { type: 'string', description: 'User ID' },
|
||||
name: { type: 'string', description: 'User name' },
|
||||
email: { type: 'string', description: 'User email' },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 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):
|
||||
|
||||
```typescript
|
||||
// V1 Block - mark as legacy
|
||||
export const ServiceBlock: BlockConfig = {
|
||||
type: 'service',
|
||||
name: 'Service (Legacy)',
|
||||
hideFromToolbar: true, // Hide from toolbar
|
||||
// ... rest of config
|
||||
}
|
||||
|
||||
// V2 Block - visible, uses V2 tools
|
||||
export const ServiceV2Block: BlockConfig = {
|
||||
type: 'service_v2',
|
||||
name: 'Service', // Clean name
|
||||
hideFromToolbar: false, // Visible
|
||||
subBlocks: ServiceBlock.subBlocks, // Reuse UI
|
||||
tools: {
|
||||
access: ServiceBlock.tools?.access?.map(id => `${id}_v2`) || [],
|
||||
config: {
|
||||
tool: createVersionedToolSelector({
|
||||
baseToolSelector: (params) => (ServiceBlock.tools?.config as any)?.tool(params),
|
||||
suffix: '_v2',
|
||||
fallbackToolId: 'service_default_v2',
|
||||
}),
|
||||
params: ServiceBlock.tools?.config?.params,
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
// Flat, API-aligned outputs (not wrapped in content/metadata)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Registering Blocks
|
||||
|
||||
After creating the block, remind the user to:
|
||||
1. Import in `apps/sim/blocks/registry.ts`
|
||||
2. Add to the `registry` object (alphabetically):
|
||||
|
||||
```typescript
|
||||
import { ServiceBlock } from '@/blocks/blocks/service'
|
||||
|
||||
export const registry: Record<string, BlockConfig> = {
|
||||
// ... existing blocks ...
|
||||
service: ServiceBlock,
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
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',
|
||||
name: 'Service',
|
||||
description: 'Integrate with Service API',
|
||||
longDescription: 'Full description for documentation...',
|
||||
docsLink: 'https://docs.sim.ai/tools/service',
|
||||
category: 'tools',
|
||||
bgColor: '#FF6B6B',
|
||||
icon: ServiceIcon,
|
||||
authMode: AuthMode.OAuth,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Create', id: 'create' },
|
||||
{ label: 'Read', id: 'read' },
|
||||
{ label: 'Update', id: 'update' },
|
||||
{ label: 'Delete', id: 'delete' },
|
||||
],
|
||||
value: () => 'create',
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Service Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'service',
|
||||
requiredScopes: getScopesForService('service'),
|
||||
placeholder: 'Select account',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'resourceId',
|
||||
title: 'Resource ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter resource ID',
|
||||
condition: { field: 'operation', value: ['read', 'update', 'delete'] },
|
||||
required: { field: 'operation', value: ['read', 'update', 'delete'] },
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
title: 'Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Resource name',
|
||||
condition: { field: 'operation', value: ['create', 'update'] },
|
||||
required: { field: 'operation', value: 'create' },
|
||||
},
|
||||
],
|
||||
|
||||
tools: {
|
||||
access: ['service_create', 'service_read', 'service_update', 'service_delete'],
|
||||
config: {
|
||||
tool: (params) => `service_${params.operation}`,
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Resource ID' },
|
||||
name: { type: 'string', description: 'Resource name' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Connecting Blocks with Triggers
|
||||
|
||||
If the service supports webhooks, connect the block to its triggers.
|
||||
|
||||
```typescript
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const ServiceBlock: BlockConfig = {
|
||||
// ... basic config ...
|
||||
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['service_event_a', 'service_event_b', 'service_webhook'],
|
||||
},
|
||||
|
||||
subBlocks: [
|
||||
// Tool subBlocks first...
|
||||
{ id: 'operation', /* ... */ },
|
||||
|
||||
// Then spread trigger subBlocks
|
||||
...getTrigger('service_event_a').subBlocks,
|
||||
...getTrigger('service_event_b').subBlocks,
|
||||
...getTrigger('service_webhook').subBlocks,
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
See the `/add-trigger` skill for creating triggers.
|
||||
|
||||
## Icon Requirement
|
||||
|
||||
If the icon doesn't already exist in `@/components/icons.tsx`, **do NOT search for it yourself**. After completing the block, ask the user to provide the SVG:
|
||||
|
||||
```
|
||||
The block is complete, but I need an icon for {Service}.
|
||||
Please provide the SVG and I'll convert it to a React component.
|
||||
|
||||
You can usually find this in the service's brand/press kit page, or copy it from their website.
|
||||
```
|
||||
|
||||
## Advanced Mode for Optional Fields
|
||||
|
||||
Optional fields that are rarely used should be set to `mode: 'advanced'` so they don't clutter the basic UI. This includes:
|
||||
- Pagination tokens
|
||||
- Time range filters (start/end time)
|
||||
- Sort order options
|
||||
- Reply settings
|
||||
- Rarely used IDs (e.g., reply-to tweet ID, quote tweet ID)
|
||||
- Max results / limits
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'startTime',
|
||||
title: 'Start Time',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 timestamp',
|
||||
condition: { field: 'operation', value: ['search', 'list'] },
|
||||
mode: 'advanced', // Rarely used, hide from basic view
|
||||
}
|
||||
```
|
||||
|
||||
## WandConfig for Complex Inputs
|
||||
|
||||
Use `wandConfig` for fields that are hard to fill out manually, such as timestamps, comma-separated lists, and complex query strings. This gives users an AI-assisted input experience.
|
||||
|
||||
```typescript
|
||||
// Timestamps - use generationType: 'timestamp' to inject current date context
|
||||
{
|
||||
id: 'startTime',
|
||||
title: 'Start Time',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 timestamp based on the user description. Return ONLY the timestamp string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
}
|
||||
|
||||
// Comma-separated lists - simple prompt without generationType
|
||||
{
|
||||
id: 'mediaIds',
|
||||
title: 'Media IDs',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate a comma-separated list of media IDs. Return ONLY the comma-separated values.',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Naming Convention
|
||||
|
||||
All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MUST use `snake_case` (e.g., `x_create_tweet`, `slack_send_message`). Never use camelCase or PascalCase.
|
||||
|
||||
## Checklist Before Finishing
|
||||
|
||||
- [ ] All subBlocks have `id`, `title` (except switch), and `type`
|
||||
- [ ] Conditions use correct syntax (field, value, not, and)
|
||||
- [ ] DependsOn set for fields that need other values
|
||||
- [ ] Required fields marked correctly (boolean or condition)
|
||||
- [ ] OAuth inputs have correct `serviceId` and `requiredScopes: getScopesForService(serviceId)`
|
||||
- [ ] Scope descriptions added to `SCOPE_DESCRIPTIONS` in `lib/oauth/utils.ts` for any new scopes
|
||||
- [ ] Tools.access lists all tool IDs (snake_case)
|
||||
- [ ] Tools.config.tool returns correct tool ID (snake_case)
|
||||
- [ ] 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
|
||||
5
.agents/skills/add-block/agents/openai.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Add Block"
|
||||
short_description: "Build a Sim block definition"
|
||||
brand_color: "#2563EB"
|
||||
default_prompt: "Use $add-block to create or update the block for a Sim integration."
|
||||
528
.agents/skills/add-connector/SKILL.md
Normal file
@@ -0,0 +1,528 @@
|
||||
---
|
||||
name: add-connector
|
||||
description: Add or update a Sim knowledge base connector for syncing documents from an external source, including auth mode, config fields, pagination, document mapping, tags, and registry wiring. Use when working in `apps/sim/connectors/{service}/` or adding a new external document source.
|
||||
---
|
||||
|
||||
# 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) => {
|
||||
// Return metadata stubs with contentDeferred: true (if per-doc content fetch needed)
|
||||
// Or full documents with content (if list API returns content inline)
|
||||
// Return { documents: ExternalDocument[], nextCursor?, hasMore }
|
||||
},
|
||||
|
||||
getDocument: async (accessToken, sourceConfig, externalId) => {
|
||||
// Fetch full content for a single document
|
||||
// Return ExternalDocument with contentDeferred: false, 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 (or '' if contentDeferred)
|
||||
contentDeferred?: boolean // true = content will be fetched via getDocument
|
||||
mimeType: 'text/plain' // Always text/plain (content is extracted)
|
||||
contentHash: string // Metadata-based hash for change detection
|
||||
sourceUrl?: string // Link back to original (stored on document record)
|
||||
metadata?: Record<string, unknown> // Source-specific data (fed to mapTags)
|
||||
}
|
||||
```
|
||||
|
||||
## Content Deferral (Required for file/content-download connectors)
|
||||
|
||||
**All connectors that require per-document API calls to fetch content MUST use `contentDeferred: true`.** This is the standard pattern — `listDocuments` returns lightweight metadata stubs, and content is fetched lazily by the sync engine via `getDocument` only for new/changed documents.
|
||||
|
||||
This pattern is critical for reliability: the sync engine processes documents in batches and enqueues each batch for processing immediately. If a sync times out, all previously-batched documents are already queued. Without deferral, content downloads during listing can exhaust the sync task's time budget before any documents are saved.
|
||||
|
||||
### When to use `contentDeferred: true`
|
||||
|
||||
- The service's list API does NOT return document content (only metadata)
|
||||
- Content requires a separate download/export API call per document
|
||||
- Examples: Google Drive, OneDrive, SharePoint, Dropbox, Notion, Confluence, Gmail, Obsidian, Evernote, GitHub
|
||||
|
||||
### When NOT to use `contentDeferred`
|
||||
|
||||
- The list API already returns the full content inline (e.g., Slack messages, Reddit posts, HubSpot notes)
|
||||
- No per-document API call is needed to get content
|
||||
|
||||
### Content Hash Strategy
|
||||
|
||||
Use a **metadata-based** `contentHash` — never a content-based hash. The hash must be derivable from the list response metadata alone, so the sync engine can detect changes without downloading content.
|
||||
|
||||
Good metadata hash sources:
|
||||
- `modifiedTime` / `lastModifiedDateTime` — changes when file is edited
|
||||
- Git blob SHA — unique per content version
|
||||
- API-provided content hash (e.g., Dropbox `content_hash`)
|
||||
- Version number (e.g., Confluence page version)
|
||||
|
||||
Format: `{service}:{id}:{changeIndicator}`
|
||||
|
||||
```typescript
|
||||
// Google Drive: modifiedTime changes on edit
|
||||
contentHash: `gdrive:${file.id}:${file.modifiedTime ?? ''}`
|
||||
|
||||
// GitHub: blob SHA is a content-addressable hash
|
||||
contentHash: `gitsha:${item.sha}`
|
||||
|
||||
// Dropbox: API provides content_hash
|
||||
contentHash: `dropbox:${entry.id}:${entry.content_hash ?? entry.server_modified}`
|
||||
|
||||
// Confluence: version number increments on edit
|
||||
contentHash: `confluence:${page.id}:${page.version.number}`
|
||||
```
|
||||
|
||||
**Critical invariant:** The `contentHash` MUST be identical whether produced by `listDocuments` (stub) or `getDocument` (full doc). Both should use the same stub function to guarantee this.
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```typescript
|
||||
// 1. Create a stub function (sync, no API calls)
|
||||
function fileToStub(file: ServiceFile): ExternalDocument {
|
||||
return {
|
||||
externalId: file.id,
|
||||
title: file.name || 'Untitled',
|
||||
content: '',
|
||||
contentDeferred: true,
|
||||
mimeType: 'text/plain',
|
||||
sourceUrl: `https://service.com/file/${file.id}`,
|
||||
contentHash: `service:${file.id}:${file.modifiedTime ?? ''}`,
|
||||
metadata: { /* fields needed by mapTags */ },
|
||||
}
|
||||
}
|
||||
|
||||
// 2. listDocuments returns stubs (fast, metadata only)
|
||||
listDocuments: async (accessToken, sourceConfig, cursor) => {
|
||||
const response = await fetchWithRetry(listUrl, { ... })
|
||||
const files = (await response.json()).files
|
||||
const documents = files.map(fileToStub)
|
||||
return { documents, nextCursor, hasMore }
|
||||
}
|
||||
|
||||
// 3. getDocument fetches content and returns full doc with SAME contentHash
|
||||
getDocument: async (accessToken, sourceConfig, externalId) => {
|
||||
const metadata = await fetchWithRetry(metadataUrl, { ... })
|
||||
const file = await metadata.json()
|
||||
if (file.trashed) return null
|
||||
|
||||
try {
|
||||
const content = await fetchContent(accessToken, file)
|
||||
if (!content.trim()) return null
|
||||
const stub = fileToStub(file)
|
||||
return { ...stub, content, contentDeferred: false }
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to fetch content for: ${file.name}`, { error })
|
||||
return null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reference Implementations
|
||||
|
||||
- **Google Drive**: `connectors/google-drive/google-drive.ts` — file download/export with `modifiedTime` hash
|
||||
- **GitHub**: `connectors/github/github.ts` — git blob SHA hash
|
||||
- **Notion**: `connectors/notion/notion.ts` — blocks API with `last_edited_time` hash
|
||||
- **Confluence**: `connectors/confluence/confluence.ts` — version number hash
|
||||
|
||||
## 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 + contentDeferred**: `apps/sim/connectors/google-drive/google-drive.ts` — file download with metadata-based hash, `orderBy` for deterministic pagination
|
||||
- **OAuth + contentDeferred (blocks API)**: `apps/sim/connectors/notion/notion.ts` — complex block content extraction deferred to `getDocument`
|
||||
- **OAuth + contentDeferred (git)**: `apps/sim/connectors/github/github.ts` — blob SHA hash, tree listing
|
||||
- **OAuth + inline content**: `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 with metadata-based content hashes
|
||||
- [ ] `contentDeferred: true` used if content requires per-doc API calls (file download, export, blocks fetch)
|
||||
- [ ] `contentHash` is metadata-based (not content-based) and identical between stub and `getDocument`
|
||||
- [ ] `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`
|
||||
5
.agents/skills/add-connector/agents/openai.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Add Connector"
|
||||
short_description: "Build a Sim knowledge connector"
|
||||
brand_color: "#0F766E"
|
||||
default_prompt: "Use $add-connector to add or update a Sim knowledge connector for a service."
|
||||
760
.agents/skills/add-integration/SKILL.md
Normal file
@@ -0,0 +1,760 @@
|
||||
---
|
||||
name: add-integration
|
||||
description: Add a complete Sim integration from API docs, covering tools, block, icon, optional triggers, registrations, and integration conventions. Use when introducing a new service under `apps/sim/tools`, `apps/sim/blocks`, and `apps/sim/triggers`.
|
||||
---
|
||||
|
||||
# Add Integration Skill
|
||||
|
||||
You are an expert at adding complete integrations to Sim. This skill orchestrates the full process of adding a new service integration.
|
||||
|
||||
## Overview
|
||||
|
||||
Adding an integration involves these steps in order:
|
||||
1. **Research** - Read the service's API documentation
|
||||
2. **Create Tools** - Build tool configurations for each API operation
|
||||
3. **Create Block** - Build the block UI configuration
|
||||
4. **Add Icon** - Add the service's brand icon
|
||||
5. **Create Triggers** (optional) - If the service supports webhooks
|
||||
6. **Register** - Register tools, block, and triggers in their registries
|
||||
7. **Generate Docs** - Run the docs generation script
|
||||
|
||||
## Step 1: Research the API
|
||||
|
||||
Before writing any code:
|
||||
1. Use Context7 to find official documentation: `mcp__plugin_context7_context7__resolve-library-id`
|
||||
2. Or use WebFetch to read API docs directly
|
||||
3. Identify:
|
||||
- Authentication method (OAuth, API Key, both)
|
||||
- Available operations (CRUD, search, etc.)
|
||||
- Required vs optional parameters
|
||||
- Response structures
|
||||
|
||||
## Step 2: Create Tools
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
apps/sim/tools/{service}/
|
||||
├── index.ts # Barrel exports
|
||||
├── types.ts # TypeScript interfaces
|
||||
├── {action1}.ts # Tool for action 1
|
||||
├── {action2}.ts # Tool for action 2
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Key Patterns
|
||||
|
||||
**types.ts:**
|
||||
```typescript
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface {Service}{Action}Params {
|
||||
accessToken: string // For OAuth services
|
||||
// OR
|
||||
apiKey: string // For API key services
|
||||
|
||||
requiredParam: string
|
||||
optionalParam?: string
|
||||
}
|
||||
|
||||
export interface {Service}Response extends ToolResponse {
|
||||
output: {
|
||||
// Define output structure
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Tool file pattern:**
|
||||
```typescript
|
||||
export const {service}{Action}Tool: ToolConfig<Params, Response> = {
|
||||
id: '{service}_{action}',
|
||||
name: '{Service} {Action}',
|
||||
description: '...',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: '{service}' }, // If OAuth
|
||||
|
||||
params: {
|
||||
accessToken: { type: 'string', required: true, visibility: 'hidden', description: '...' },
|
||||
// ... other params
|
||||
},
|
||||
|
||||
request: { url, method, headers, body },
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
field: data.field ?? null, // Always handle nullables
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
### Critical Rules
|
||||
- `visibility: 'hidden'` for OAuth tokens
|
||||
- `visibility: 'user-only'` for API keys and user credentials
|
||||
- `visibility: 'user-or-llm'` for operation parameters
|
||||
- Always use `?? null` for nullable API response fields
|
||||
- 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
|
||||
|
||||
### File Location
|
||||
`apps/sim/blocks/blocks/{service}.ts`
|
||||
|
||||
### Block Structure
|
||||
```typescript
|
||||
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}',
|
||||
name: '{Service}',
|
||||
description: '...',
|
||||
longDescription: '...',
|
||||
docsLink: 'https://docs.sim.ai/tools/{service}',
|
||||
category: 'tools',
|
||||
bgColor: '#HEXCOLOR',
|
||||
icon: {Service}Icon,
|
||||
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
|
||||
|
||||
subBlocks: [
|
||||
// Operation dropdown
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Operation 1', id: 'action1' },
|
||||
{ label: 'Operation 2', id: 'action2' },
|
||||
],
|
||||
value: () => 'action1',
|
||||
},
|
||||
// Credential field
|
||||
{
|
||||
id: 'credential',
|
||||
title: '{Service} Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: '{service}',
|
||||
requiredScopes: getScopesForService('{service}'),
|
||||
required: true,
|
||||
},
|
||||
// Conditional fields per operation
|
||||
// ...
|
||||
],
|
||||
|
||||
tools: {
|
||||
access: ['{service}_action1', '{service}_action2'],
|
||||
config: {
|
||||
tool: (params) => `{service}_${params.operation}`,
|
||||
},
|
||||
},
|
||||
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
### Key SubBlock Patterns
|
||||
|
||||
**Condition-based visibility:**
|
||||
```typescript
|
||||
{
|
||||
id: 'resourceId',
|
||||
title: 'Resource ID',
|
||||
type: 'short-input',
|
||||
condition: { field: 'operation', value: ['read', 'update', 'delete'] },
|
||||
required: { field: 'operation', value: ['read', 'update', 'delete'] },
|
||||
}
|
||||
```
|
||||
|
||||
**DependsOn for cascading selectors:**
|
||||
```typescript
|
||||
{
|
||||
id: 'project',
|
||||
type: 'project-selector',
|
||||
dependsOn: ['credential'],
|
||||
},
|
||||
{
|
||||
id: 'issue',
|
||||
type: 'file-selector',
|
||||
dependsOn: ['credential', 'project'],
|
||||
}
|
||||
```
|
||||
|
||||
**Basic/Advanced mode for dual UX:**
|
||||
```typescript
|
||||
// Basic: Visual selector
|
||||
{
|
||||
id: 'channel',
|
||||
type: 'channel-selector',
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'channel',
|
||||
dependsOn: ['credential'],
|
||||
},
|
||||
// Advanced: Manual input
|
||||
{
|
||||
id: 'channelId',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
canonicalParamId: 'channel',
|
||||
}
|
||||
```
|
||||
|
||||
**Critical Canonical Param Rules:**
|
||||
- `canonicalParamId` must NOT match any subblock's `id` in the block
|
||||
- `canonicalParamId` must be unique per operation/condition context
|
||||
- Only use `canonicalParamId` to link basic/advanced alternatives for the same logical parameter
|
||||
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent
|
||||
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions
|
||||
- **Required consistency:** If one subblock in a canonical group has `required: true`, ALL subblocks in that group must have `required: true` (prevents bypassing validation by switching modes)
|
||||
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs (e.g., `fileSelector`, `manualFileId`)
|
||||
- **Params function:** Must use canonical param IDs, NOT raw subblock IDs (raw IDs are deleted after canonical transformation)
|
||||
|
||||
## Step 4: Add Icon
|
||||
|
||||
### File Location
|
||||
`apps/sim/components/icons.tsx`
|
||||
|
||||
### Pattern
|
||||
```typescript
|
||||
export function {Service}Icon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{/* SVG paths from user-provided SVG */}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Getting Icons
|
||||
**Do NOT search for icons yourself.** At the end of implementation, ask the user to provide the SVG:
|
||||
|
||||
```
|
||||
I've completed the integration. Before I can add the icon, please provide the SVG for {Service}.
|
||||
You can usually find this in the service's brand/press kit page, or copy it from their website.
|
||||
|
||||
Paste the SVG code here and I'll convert it to a React component.
|
||||
```
|
||||
|
||||
Once the user provides the SVG:
|
||||
1. Extract the SVG paths/content
|
||||
2. Create a React component that spreads props
|
||||
3. Ensure viewBox is preserved from the original SVG
|
||||
|
||||
## Step 5: Create Triggers (Optional)
|
||||
|
||||
If the service supports webhooks, create triggers using the generic `buildTriggerSubBlocks` helper.
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
apps/sim/triggers/{service}/
|
||||
├── index.ts # Barrel exports
|
||||
├── utils.ts # Trigger options, setup instructions, extra fields
|
||||
├── {event_a}.ts # Primary trigger (includes dropdown)
|
||||
├── {event_b}.ts # Secondary triggers (no dropdown)
|
||||
└── webhook.ts # Generic webhook (optional)
|
||||
```
|
||||
|
||||
### Key Pattern
|
||||
|
||||
```typescript
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import { {service}TriggerOptions, {service}SetupInstructions, build{Service}ExtraFields } from './utils'
|
||||
|
||||
// Primary trigger - includeDropdown: true
|
||||
export const {service}EventATrigger: TriggerConfig = {
|
||||
id: '{service}_event_a',
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: '{service}_event_a',
|
||||
triggerOptions: {service}TriggerOptions,
|
||||
includeDropdown: true, // Only for primary trigger!
|
||||
setupInstructions: {service}SetupInstructions('Event A'),
|
||||
extraFields: build{Service}ExtraFields('{service}_event_a'),
|
||||
}),
|
||||
// ...
|
||||
}
|
||||
|
||||
// Secondary triggers - no dropdown
|
||||
export const {service}EventBTrigger: TriggerConfig = {
|
||||
id: '{service}_event_b',
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: '{service}_event_b',
|
||||
triggerOptions: {service}TriggerOptions,
|
||||
// No includeDropdown!
|
||||
setupInstructions: {service}SetupInstructions('Event B'),
|
||||
extraFields: build{Service}ExtraFields('{service}_event_b'),
|
||||
}),
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Connect to Block
|
||||
```typescript
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const {Service}Block: BlockConfig = {
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['{service}_event_a', '{service}_event_b'],
|
||||
},
|
||||
subBlocks: [
|
||||
// Tool fields...
|
||||
...getTrigger('{service}_event_a').subBlocks,
|
||||
...getTrigger('{service}_event_b').subBlocks,
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
See `/add-trigger` skill for complete documentation.
|
||||
|
||||
## Step 6: Register Everything
|
||||
|
||||
### Tools Registry (`apps/sim/tools/registry.ts`)
|
||||
|
||||
```typescript
|
||||
// Add import (alphabetically)
|
||||
import {
|
||||
{service}Action1Tool,
|
||||
{service}Action2Tool,
|
||||
} from '@/tools/{service}'
|
||||
|
||||
// Add to tools object (alphabetically)
|
||||
export const tools: Record<string, ToolConfig> = {
|
||||
// ... existing tools ...
|
||||
{service}_action1: {service}Action1Tool,
|
||||
{service}_action2: {service}Action2Tool,
|
||||
}
|
||||
```
|
||||
|
||||
### Block Registry (`apps/sim/blocks/registry.ts`)
|
||||
|
||||
```typescript
|
||||
// Add import (alphabetically)
|
||||
import { {Service}Block } from '@/blocks/blocks/{service}'
|
||||
|
||||
// Add to registry (alphabetically)
|
||||
export const registry: Record<string, BlockConfig> = {
|
||||
// ... existing blocks ...
|
||||
{service}: {Service}Block,
|
||||
}
|
||||
```
|
||||
|
||||
### Trigger Registry (`apps/sim/triggers/registry.ts`) - If triggers exist
|
||||
|
||||
```typescript
|
||||
// Add import (alphabetically)
|
||||
import {
|
||||
{service}EventATrigger,
|
||||
{service}EventBTrigger,
|
||||
{service}WebhookTrigger,
|
||||
} from '@/triggers/{service}'
|
||||
|
||||
// Add to TRIGGER_REGISTRY (alphabetically)
|
||||
export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
// ... existing triggers ...
|
||||
{service}_event_a: {service}EventATrigger,
|
||||
{service}_event_b: {service}EventBTrigger,
|
||||
{service}_webhook: {service}WebhookTrigger,
|
||||
}
|
||||
```
|
||||
|
||||
## Step 7: Generate Docs
|
||||
|
||||
Run the documentation generator:
|
||||
```bash
|
||||
bun run scripts/generate-docs.ts
|
||||
```
|
||||
|
||||
This creates `apps/docs/content/docs/en/tools/{service}.mdx`
|
||||
|
||||
## V2 Integration Pattern
|
||||
|
||||
If creating V2 versions (API-aligned outputs):
|
||||
|
||||
1. **V2 Tools** - Add `_v2` suffix, version `2.0.0`, flat outputs
|
||||
2. **V2 Block** - Add `_v2` type, use `createVersionedToolSelector`
|
||||
3. **V1 Block** - Add `(Legacy)` to name, set `hideFromToolbar: true`
|
||||
4. **Registry** - Register both versions
|
||||
|
||||
```typescript
|
||||
// In registry
|
||||
{service}: {Service}Block, // V1 (legacy, hidden)
|
||||
{service}_v2: {Service}V2Block, // V2 (visible)
|
||||
```
|
||||
|
||||
## Complete Checklist
|
||||
|
||||
### Tools
|
||||
- [ ] Created `tools/{service}/` directory
|
||||
- [ ] Created `types.ts` with all interfaces
|
||||
- [ ] Created tool file for each operation
|
||||
- [ ] All params have correct visibility
|
||||
- [ ] All nullable fields use `?? null`
|
||||
- [ ] All optional outputs have `optional: true`
|
||||
- [ ] Created `index.ts` barrel export
|
||||
- [ ] Registered all tools in `tools/registry.ts`
|
||||
|
||||
### Block
|
||||
- [ ] Created `blocks/blocks/{service}.ts`
|
||||
- [ ] Defined operation dropdown with all operations
|
||||
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
|
||||
- [ ] Added conditional fields per operation
|
||||
- [ ] Set up dependsOn for cascading selectors
|
||||
- [ ] Configured tools.access with all tool IDs
|
||||
- [ ] Configured tools.config.tool selector
|
||||
- [ ] Defined outputs matching tool outputs
|
||||
- [ ] Registered block in `blocks/registry.ts`
|
||||
- [ ] If triggers: set `triggers.enabled` and `triggers.available`
|
||||
- [ ] If triggers: spread trigger subBlocks with `getTrigger()`
|
||||
|
||||
### OAuth Scopes (if OAuth service)
|
||||
- [ ] Defined scopes in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS`
|
||||
- [ ] Added scope descriptions in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
|
||||
- [ ] Used `getCanonicalScopesForProvider()` in `auth.ts` (never hardcode)
|
||||
- [ ] Used `getScopesForService()` in block `requiredScopes` (never hardcode)
|
||||
|
||||
### Icon
|
||||
- [ ] Asked user to provide SVG
|
||||
- [ ] Added icon to `components/icons.tsx`
|
||||
- [ ] Icon spreads props correctly
|
||||
|
||||
### Triggers (if service supports webhooks)
|
||||
- [ ] Created `triggers/{service}/` directory
|
||||
- [ ] Created `utils.ts` with options, instructions, and extra fields helpers
|
||||
- [ ] Primary trigger uses `includeDropdown: true`
|
||||
- [ ] Secondary triggers do NOT have `includeDropdown`
|
||||
- [ ] All triggers use `buildTriggerSubBlocks` helper
|
||||
- [ ] Created `index.ts` barrel export
|
||||
- [ ] Registered all triggers in `triggers/registry.ts`
|
||||
|
||||
### Docs
|
||||
- [ ] 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:
|
||||
|
||||
```
|
||||
User: Add a Stripe integration
|
||||
|
||||
You: I'll add the Stripe integration. Let me:
|
||||
|
||||
1. First, research the Stripe API using Context7
|
||||
2. Create the tools for key operations (payments, subscriptions, etc.)
|
||||
3. Create the block with operation dropdown
|
||||
4. Register everything
|
||||
5. Generate docs
|
||||
6. Ask you for the Stripe icon SVG
|
||||
|
||||
[Proceed with implementation...]
|
||||
|
||||
[After completing steps 1-5...]
|
||||
|
||||
I've completed the Stripe integration. Before I can add the icon, please provide the SVG for Stripe.
|
||||
You can usually find this in the service's brand/press kit page, or copy it from their website.
|
||||
|
||||
Paste the SVG code here and I'll convert it to a React component.
|
||||
```
|
||||
|
||||
## File Handling
|
||||
|
||||
When your integration handles file uploads or downloads, follow these patterns to work with `UserFile` objects consistently.
|
||||
|
||||
### What is a UserFile?
|
||||
|
||||
A `UserFile` is the standard file representation in Sim:
|
||||
|
||||
```typescript
|
||||
interface UserFile {
|
||||
id: string // Unique identifier
|
||||
name: string // Original filename
|
||||
url: string // Presigned URL for download
|
||||
size: number // File size in bytes
|
||||
type: string // MIME type (e.g., 'application/pdf')
|
||||
base64?: string // Optional base64 content (if small file)
|
||||
key?: string // Internal storage key
|
||||
context?: object // Storage context metadata
|
||||
}
|
||||
```
|
||||
|
||||
### File Input Pattern (Uploads)
|
||||
|
||||
For tools that accept file uploads, **always route through an internal API endpoint** rather than calling external APIs directly. This ensures proper file content retrieval.
|
||||
|
||||
#### 1. Block SubBlocks for File Input
|
||||
|
||||
Use the basic/advanced mode pattern:
|
||||
|
||||
```typescript
|
||||
// Basic mode: File upload UI
|
||||
{
|
||||
id: 'uploadFile',
|
||||
title: 'File',
|
||||
type: 'file-upload',
|
||||
canonicalParamId: 'file', // Maps to 'file' param
|
||||
placeholder: 'Upload file',
|
||||
mode: 'basic',
|
||||
multiple: false,
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'upload' },
|
||||
},
|
||||
// Advanced mode: Reference from previous block
|
||||
{
|
||||
id: 'fileRef',
|
||||
title: 'File',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'file', // Same canonical param
|
||||
placeholder: 'Reference file (e.g., {{file_block.output}})',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'upload' },
|
||||
},
|
||||
```
|
||||
|
||||
**Critical:** `canonicalParamId` must NOT match any subblock `id`.
|
||||
|
||||
#### 2. Normalize File Input in Block Config
|
||||
|
||||
In `tools.config.tool`, use `normalizeFileInput` to handle all input variants:
|
||||
|
||||
```typescript
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
|
||||
tools: {
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Normalize file from basic (uploadFile), advanced (fileRef), or legacy (fileContent)
|
||||
const normalizedFile = normalizeFileInput(
|
||||
params.uploadFile || params.fileRef || params.fileContent,
|
||||
{ single: true }
|
||||
)
|
||||
if (normalizedFile) {
|
||||
params.file = normalizedFile
|
||||
}
|
||||
return `{service}_${params.operation}`
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Create Internal API Route
|
||||
|
||||
Create `apps/sim/app/api/tools/{service}/{action}/route.ts`:
|
||||
|
||||
```typescript
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { FileInputSchema, type RawFileInput } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
|
||||
const logger = createLogger('{Service}UploadAPI')
|
||||
|
||||
const RequestSchema = z.object({
|
||||
accessToken: z.string(),
|
||||
file: FileInputSchema.optional().nullable(),
|
||||
// Legacy field for backwards compatibility
|
||||
fileContent: z.string().optional().nullable(),
|
||||
// ... other params
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = RequestSchema.parse(body)
|
||||
|
||||
let fileBuffer: Buffer
|
||||
let fileName: string
|
||||
|
||||
// Prefer UserFile input, fall back to legacy base64
|
||||
if (data.file) {
|
||||
const userFiles = processFilesToUserFiles([data.file as RawFileInput], requestId, logger)
|
||||
if (userFiles.length === 0) {
|
||||
return NextResponse.json({ success: false, error: 'Invalid file' }, { status: 400 })
|
||||
}
|
||||
const userFile = userFiles[0]
|
||||
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
fileName = userFile.name
|
||||
} else if (data.fileContent) {
|
||||
// Legacy: base64 string (backwards compatibility)
|
||||
fileBuffer = Buffer.from(data.fileContent, 'base64')
|
||||
fileName = 'file'
|
||||
} else {
|
||||
return NextResponse.json({ success: false, error: 'File required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Now call external API with fileBuffer
|
||||
const response = await fetch('https://api.{service}.com/upload', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${data.accessToken}` },
|
||||
body: new Uint8Array(fileBuffer), // Convert Buffer for fetch
|
||||
})
|
||||
|
||||
// ... handle response
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Update Tool to Use Internal Route
|
||||
|
||||
```typescript
|
||||
export const {service}UploadTool: ToolConfig<Params, Response> = {
|
||||
id: '{service}_upload',
|
||||
// ...
|
||||
params: {
|
||||
file: { type: 'file', required: false, visibility: 'user-or-llm' },
|
||||
fileContent: { type: 'string', required: false, visibility: 'hidden' }, // Legacy
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/{service}/upload', // Internal route
|
||||
method: 'POST',
|
||||
body: (params) => ({
|
||||
accessToken: params.accessToken,
|
||||
file: params.file,
|
||||
fileContent: params.fileContent,
|
||||
}),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### File Output Pattern (Downloads)
|
||||
|
||||
For tools that return files, use `FileToolProcessor` to store files and return `UserFile` objects.
|
||||
|
||||
#### In Tool transformResponse
|
||||
|
||||
```typescript
|
||||
import { FileToolProcessor } from '@/executor/utils/file-tool-processor'
|
||||
|
||||
transformResponse: async (response, context) => {
|
||||
const data = await response.json()
|
||||
|
||||
// Process file outputs to UserFile objects
|
||||
const fileProcessor = new FileToolProcessor(context)
|
||||
const file = await fileProcessor.processFileData({
|
||||
data: data.content, // base64 or buffer
|
||||
mimeType: data.mimeType,
|
||||
filename: data.filename,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { file },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### In API Route (for complex file handling)
|
||||
|
||||
```typescript
|
||||
// Return file data that FileToolProcessor can handle
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
file: {
|
||||
data: base64Content,
|
||||
mimeType: 'application/pdf',
|
||||
filename: 'document.pdf',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Key Helpers Reference
|
||||
|
||||
| Helper | Location | Purpose |
|
||||
|--------|----------|---------|
|
||||
| `normalizeFileInput` | `@/blocks/utils` | Normalize file params in block config |
|
||||
| `processFilesToUserFiles` | `@/lib/uploads/utils/file-utils` | Convert raw inputs to UserFile[] |
|
||||
| `downloadFileFromStorage` | `@/lib/uploads/utils/file-utils.server` | Get file Buffer from UserFile |
|
||||
| `FileToolProcessor` | `@/executor/utils/file-tool-processor` | Process tool output files |
|
||||
| `isUserFile` | `@/lib/core/utils/user-file` | Type guard for UserFile objects |
|
||||
| `FileInputSchema` | `@/lib/uploads/utils/file-schemas` | Zod schema for file validation |
|
||||
|
||||
### Advanced Mode for Optional Fields
|
||||
|
||||
Optional fields that are rarely used should be set to `mode: 'advanced'` so they don't clutter the basic UI. Examples: pagination tokens, time range filters, sort order, max results, reply settings.
|
||||
|
||||
### WandConfig for Complex Inputs
|
||||
|
||||
Use `wandConfig` for fields that are hard to fill out manually:
|
||||
- **Timestamps**: Use `generationType: 'timestamp'` to inject current date context into the AI prompt
|
||||
- **JSON arrays**: Use `generationType: 'json-object'` for structured data
|
||||
- **Complex queries**: Use a descriptive prompt explaining the expected format
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'startTime',
|
||||
title: 'Start Time',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth Scopes (Centralized System)
|
||||
|
||||
Scopes are maintained in a single source of truth and reused everywhere:
|
||||
|
||||
1. **Define scopes** in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS[provider].services[service].scopes`
|
||||
2. **Add descriptions** in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts` for the OAuth modal UI
|
||||
3. **Reference in auth.ts** using `getCanonicalScopesForProvider(providerId)` from `@/lib/oauth/utils`
|
||||
4. **Reference in blocks** using `getScopesForService(serviceId)` from `@/lib/oauth/utils`
|
||||
|
||||
**Never hardcode scope arrays** in `auth.ts` or block `requiredScopes`. Always import from the centralized source.
|
||||
|
||||
```typescript
|
||||
// In auth.ts (Better Auth config)
|
||||
scopes: getCanonicalScopesForProvider('{service}'),
|
||||
|
||||
// In block credential sub-block
|
||||
requiredScopes: getScopesForService('{service}'),
|
||||
```
|
||||
|
||||
### Common Gotchas
|
||||
|
||||
1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration
|
||||
2. **All tool IDs MUST be snake_case** - `stripe_create_payment`, not `stripeCreatePayment`. This applies to tool `id` fields, registry keys, `tools.access` arrays, and `tools.config.tool` return values
|
||||
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`
|
||||
5
.agents/skills/add-integration/agents/openai.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Add Integration"
|
||||
short_description: "Build a full Sim integration"
|
||||
brand_color: "#7C3AED"
|
||||
default_prompt: "Use $add-integration to add a complete Sim integration for a service."
|
||||
321
.agents/skills/add-tools/SKILL.md
Normal file
@@ -0,0 +1,321 @@
|
||||
---
|
||||
name: add-tools
|
||||
description: Create or update Sim tool configurations from service API docs, including typed params, request mapping, response transforms, outputs, and registry entries. Use when working in `apps/sim/tools/{service}/` or fixing tool definitions for an integration.
|
||||
---
|
||||
|
||||
# Add Tools Skill
|
||||
|
||||
You are an expert at creating tool configurations for Sim integrations. Your job is to read API documentation and create properly structured tool files.
|
||||
|
||||
## Your Task
|
||||
|
||||
When the user asks you to create tools for a service:
|
||||
1. Use Context7 or WebFetch to read the service's API documentation
|
||||
2. Create the tools directory structure
|
||||
3. Generate properly typed tool configurations
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Create files in `apps/sim/tools/{service}/`:
|
||||
```
|
||||
tools/{service}/
|
||||
├── index.ts # Barrel export
|
||||
├── types.ts # Parameter & response types
|
||||
└── {action}.ts # Individual tool files (one per operation)
|
||||
```
|
||||
|
||||
## Tool Configuration Structure
|
||||
|
||||
Every tool MUST follow this exact structure:
|
||||
|
||||
```typescript
|
||||
import type { {ServiceName}{Action}Params } from '@/tools/{service}/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface {ServiceName}{Action}Response {
|
||||
success: boolean
|
||||
output: {
|
||||
// Define output structure here
|
||||
}
|
||||
}
|
||||
|
||||
export const {serviceName}{Action}Tool: ToolConfig<
|
||||
{ServiceName}{Action}Params,
|
||||
{ServiceName}{Action}Response
|
||||
> = {
|
||||
id: '{service}_{action}', // snake_case, matches tool name
|
||||
name: '{Service} {Action}', // Human readable
|
||||
description: 'Brief description', // One sentence
|
||||
version: '1.0.0',
|
||||
|
||||
// OAuth config (if service uses OAuth)
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: '{service}', // Must match OAuth provider ID
|
||||
},
|
||||
|
||||
params: {
|
||||
// Hidden params (system-injected, only use hidden for oauth accessToken)
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
// User-only params (credentials, api key, IDs user must provide)
|
||||
someId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'The ID of the resource',
|
||||
},
|
||||
// User-or-LLM params (everything else, can be provided by user OR computed by LLM)
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false, // Use false for optional
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.service.com/v1/resource/${params.id}`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
// Request body - only for POST/PUT/PATCH
|
||||
// Trim ID fields to prevent copy-paste whitespace errors:
|
||||
// userId: params.userId?.trim(),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
// Map API response to output
|
||||
// Use ?? null for nullable fields
|
||||
// Use ?? [] for optional arrays
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
// Define each output field
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Critical Rules for Parameters
|
||||
|
||||
### Visibility Options
|
||||
- `'hidden'` - System-injected (OAuth tokens, internal params). User never sees.
|
||||
- `'user-only'` - User must provide (credentials, api keys, account-specific IDs)
|
||||
- `'user-or-llm'` - User provides OR LLM can compute (search queries, content, filters, most fall into this category)
|
||||
|
||||
### Parameter Types
|
||||
- `'string'` - Text values
|
||||
- `'number'` - Numeric values
|
||||
- `'boolean'` - True/false
|
||||
- `'json'` - Complex objects (NOT 'object', use 'json')
|
||||
- `'file'` - Single file
|
||||
- `'file[]'` - Multiple files
|
||||
|
||||
### Required vs Optional
|
||||
- Always explicitly set `required: true` or `required: false`
|
||||
- Optional params should have `required: false`
|
||||
|
||||
## Critical Rules for Outputs
|
||||
|
||||
### Output Types
|
||||
- `'string'`, `'number'`, `'boolean'` - Primitives
|
||||
- `'json'` - Complex objects (use this, NOT 'object')
|
||||
- `'array'` - Arrays with `items` property
|
||||
- `'object'` - Objects with `properties` property
|
||||
|
||||
### Optional Outputs
|
||||
Add `optional: true` for fields that may not exist in the response:
|
||||
```typescript
|
||||
closedAt: {
|
||||
type: 'string',
|
||||
description: 'When the issue was closed',
|
||||
optional: true,
|
||||
},
|
||||
```
|
||||
|
||||
### 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',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Unique ID' },
|
||||
status: { type: 'string', description: 'Current status' },
|
||||
count: { type: 'number', description: 'Total count' },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
For arrays of objects, define the item structure:
|
||||
```typescript
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'List of items',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Item ID' },
|
||||
name: { type: 'string', description: 'Item name' },
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
Only use bare `type: 'json'` without `properties` when the shape is truly dynamic or unknown.
|
||||
|
||||
## Critical Rules for transformResponse
|
||||
|
||||
### Handle Nullable Fields
|
||||
ALWAYS use `?? null` for fields that may be undefined:
|
||||
```typescript
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
body: data.body ?? null, // May be undefined
|
||||
assignee: data.assignee ?? null, // May be undefined
|
||||
labels: data.labels ?? [], // Default to empty array
|
||||
closedAt: data.closed_at ?? null, // May be undefined
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Never Output Raw JSON Dumps
|
||||
DON'T do this:
|
||||
```typescript
|
||||
output: {
|
||||
data: data, // BAD - raw JSON dump
|
||||
}
|
||||
```
|
||||
|
||||
DO this instead - extract meaningful fields:
|
||||
```typescript
|
||||
output: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: data.status,
|
||||
metadata: {
|
||||
createdAt: data.created_at,
|
||||
updatedAt: data.updated_at,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Types File Pattern
|
||||
|
||||
Create `types.ts` with interfaces for all params and responses:
|
||||
|
||||
```typescript
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
// Parameter interfaces
|
||||
export interface {Service}{Action}Params {
|
||||
accessToken: string
|
||||
requiredField: string
|
||||
optionalField?: string
|
||||
}
|
||||
|
||||
// Response interfaces (extend ToolResponse)
|
||||
export interface {Service}{Action}Response extends ToolResponse {
|
||||
output: {
|
||||
field1: string
|
||||
field2: number
|
||||
optionalField?: string | null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Index.ts Barrel Export Pattern
|
||||
|
||||
```typescript
|
||||
// Export all tools
|
||||
export { serviceTool1 } from './{action1}'
|
||||
export { serviceTool2 } from './{action2}'
|
||||
|
||||
// Export types
|
||||
export * from './types'
|
||||
```
|
||||
|
||||
## Registering Tools
|
||||
|
||||
After creating tools, remind the user to:
|
||||
1. Import tools in `apps/sim/tools/registry.ts`
|
||||
2. Add to the `tools` object with snake_case keys:
|
||||
```typescript
|
||||
import { serviceActionTool } from '@/tools/{service}'
|
||||
|
||||
export const tools = {
|
||||
// ... existing tools ...
|
||||
{service}_{action}: serviceActionTool,
|
||||
}
|
||||
```
|
||||
|
||||
## V2 Tool Pattern
|
||||
|
||||
If creating V2 tools (API-aligned outputs), use `_v2` suffix:
|
||||
- Tool ID: `{service}_{action}_v2`
|
||||
- Variable name: `{action}V2Tool`
|
||||
- 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`
|
||||
- [ ] All optional outputs have `optional: true`
|
||||
- [ ] No raw JSON dumps in outputs
|
||||
- [ ] Types file has all interfaces
|
||||
- [ ] Index.ts exports all tools
|
||||
|
||||
## Final Validation (Required)
|
||||
|
||||
After creating all tools, you MUST validate every tool before finishing:
|
||||
|
||||
1. **Read every tool file** you created — do not skip any
|
||||
2. **Cross-reference with the API docs** to verify:
|
||||
- All required params are marked `required: true`
|
||||
- All optional params are marked `required: false`
|
||||
- Param types match the API (string, number, boolean, json)
|
||||
- Request URL, method, headers, and body match the API spec
|
||||
- `transformResponse` extracts the correct fields from the API response
|
||||
- All output fields match what the API actually returns
|
||||
- No fields are missing from outputs that the API provides
|
||||
- No extra fields are defined in outputs that the API doesn't return
|
||||
3. **Verify consistency** across tools:
|
||||
- Shared types in `types.ts` match all tools that use them
|
||||
- Tool IDs in the barrel export match the tool file definitions
|
||||
- Error handling is consistent (error checks, meaningful messages)
|
||||
5
.agents/skills/add-tools/agents/openai.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Add Tools"
|
||||
short_description: "Build Sim tools from API docs"
|
||||
brand_color: "#EA580C"
|
||||
default_prompt: "Use $add-tools to create or update Sim tool definitions from service API docs."
|
||||
708
.agents/skills/add-trigger/SKILL.md
Normal file
@@ -0,0 +1,708 @@
|
||||
---
|
||||
name: add-trigger
|
||||
description: Create or update Sim webhook triggers using the generic trigger builder, service-specific setup instructions, outputs, and registry wiring. Use when working in `apps/sim/triggers/{service}/` or adding webhook support to an integration.
|
||||
---
|
||||
|
||||
# Add Trigger Skill
|
||||
|
||||
You are an expert at creating webhook triggers for Sim. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks.
|
||||
|
||||
## Your Task
|
||||
|
||||
When the user asks you to create triggers for a service:
|
||||
1. Research what webhook events the service supports
|
||||
2. Create the trigger files using the generic builder
|
||||
3. Register triggers and connect them to the block
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
apps/sim/triggers/{service}/
|
||||
├── index.ts # Barrel exports
|
||||
├── utils.ts # Service-specific helpers (trigger options, setup instructions, extra fields)
|
||||
├── {event_a}.ts # Primary trigger (includes dropdown)
|
||||
├── {event_b}.ts # Secondary trigger (no dropdown)
|
||||
├── {event_c}.ts # Secondary trigger (no dropdown)
|
||||
└── webhook.ts # Generic webhook trigger (optional, for "all events")
|
||||
```
|
||||
|
||||
## Step 1: Create utils.ts
|
||||
|
||||
This file contains service-specific helpers used by all triggers.
|
||||
|
||||
```typescript
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { TriggerOutput } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Dropdown options for the trigger type selector.
|
||||
* These appear in the primary trigger's dropdown.
|
||||
*/
|
||||
export const {service}TriggerOptions = [
|
||||
{ label: 'Event A', id: '{service}_event_a' },
|
||||
{ label: 'Event B', id: '{service}_event_b' },
|
||||
{ label: 'Event C', id: '{service}_event_c' },
|
||||
{ label: 'Generic Webhook (All Events)', id: '{service}_webhook' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Generates HTML setup instructions for the trigger.
|
||||
* Displayed to users to help them configure webhooks in the external service.
|
||||
*/
|
||||
export function {service}SetupInstructions(eventType: string): string {
|
||||
const instructions = [
|
||||
'Copy the <strong>Webhook URL</strong> above',
|
||||
'Go to <strong>{Service} Settings > Webhooks</strong>',
|
||||
'Click <strong>Add Webhook</strong>',
|
||||
'Paste the webhook URL',
|
||||
`Select the <strong>${eventType}</strong> event type`,
|
||||
'Save the webhook configuration',
|
||||
'Click "Save" above to activate your trigger',
|
||||
]
|
||||
|
||||
return instructions
|
||||
.map((instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Service-specific extra fields to add to triggers.
|
||||
* These are inserted between webhookUrl and triggerSave.
|
||||
*/
|
||||
export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
|
||||
return [
|
||||
{
|
||||
id: 'projectId',
|
||||
title: 'Project ID (Optional)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Leave empty for all projects',
|
||||
description: 'Optionally filter to a specific project',
|
||||
mode: 'trigger',
|
||||
condition: { field: 'selectedTriggerId', value: triggerId },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for this trigger type.
|
||||
* Outputs define what data is available to downstream blocks.
|
||||
*/
|
||||
export function build{Service}Outputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
eventType: { type: 'string', description: 'The type of event that triggered this workflow' },
|
||||
resourceId: { type: 'string', description: 'ID of the affected resource' },
|
||||
timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' },
|
||||
// Nested outputs for complex data
|
||||
resource: {
|
||||
id: { type: 'string', description: 'Resource ID' },
|
||||
name: { type: 'string', description: 'Resource name' },
|
||||
status: { type: 'string', description: 'Current status' },
|
||||
},
|
||||
webhook: { type: 'json', description: 'Full webhook payload' },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2: Create the Primary Trigger
|
||||
|
||||
The **primary trigger** is the first one listed. It MUST include `includeDropdown: true` so users can switch between trigger types.
|
||||
|
||||
```typescript
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
build{Service}ExtraFields,
|
||||
build{Service}Outputs,
|
||||
{service}SetupInstructions,
|
||||
{service}TriggerOptions,
|
||||
} from '@/triggers/{service}/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* {Service} Event A Trigger
|
||||
*
|
||||
* This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
|
||||
*/
|
||||
export const {service}EventATrigger: TriggerConfig = {
|
||||
id: '{service}_event_a',
|
||||
name: '{Service} Event A',
|
||||
provider: '{service}',
|
||||
description: 'Trigger workflow when Event A occurs',
|
||||
version: '1.0.0',
|
||||
icon: {Service}Icon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: '{service}_event_a',
|
||||
triggerOptions: {service}TriggerOptions,
|
||||
includeDropdown: true, // PRIMARY TRIGGER - includes dropdown
|
||||
setupInstructions: {service}SetupInstructions('Event A'),
|
||||
extraFields: build{Service}ExtraFields('{service}_event_a'),
|
||||
}),
|
||||
|
||||
outputs: build{Service}Outputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Create Secondary Triggers
|
||||
|
||||
Secondary triggers do NOT include the dropdown (it's already in the primary trigger).
|
||||
|
||||
```typescript
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
build{Service}ExtraFields,
|
||||
build{Service}Outputs,
|
||||
{service}SetupInstructions,
|
||||
{service}TriggerOptions,
|
||||
} from '@/triggers/{service}/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* {Service} Event B Trigger
|
||||
*/
|
||||
export const {service}EventBTrigger: TriggerConfig = {
|
||||
id: '{service}_event_b',
|
||||
name: '{Service} Event B',
|
||||
provider: '{service}',
|
||||
description: 'Trigger workflow when Event B occurs',
|
||||
version: '1.0.0',
|
||||
icon: {Service}Icon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: '{service}_event_b',
|
||||
triggerOptions: {service}TriggerOptions,
|
||||
// NO includeDropdown - secondary trigger
|
||||
setupInstructions: {service}SetupInstructions('Event B'),
|
||||
extraFields: build{Service}ExtraFields('{service}_event_b'),
|
||||
}),
|
||||
|
||||
outputs: build{Service}Outputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Create index.ts Barrel Export
|
||||
|
||||
```typescript
|
||||
export { {service}EventATrigger } from './event_a'
|
||||
export { {service}EventBTrigger } from './event_b'
|
||||
export { {service}EventCTrigger } from './event_c'
|
||||
export { {service}WebhookTrigger } from './webhook'
|
||||
```
|
||||
|
||||
## Step 5: Register Triggers
|
||||
|
||||
### Trigger Registry (`apps/sim/triggers/registry.ts`)
|
||||
|
||||
```typescript
|
||||
// Add import
|
||||
import {
|
||||
{service}EventATrigger,
|
||||
{service}EventBTrigger,
|
||||
{service}EventCTrigger,
|
||||
{service}WebhookTrigger,
|
||||
} from '@/triggers/{service}'
|
||||
|
||||
// Add to TRIGGER_REGISTRY
|
||||
export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
// ... existing triggers ...
|
||||
{service}_event_a: {service}EventATrigger,
|
||||
{service}_event_b: {service}EventBTrigger,
|
||||
{service}_event_c: {service}EventCTrigger,
|
||||
{service}_webhook: {service}WebhookTrigger,
|
||||
}
|
||||
```
|
||||
|
||||
## Step 6: Connect Triggers to Block
|
||||
|
||||
In the block file (`apps/sim/blocks/blocks/{service}.ts`):
|
||||
|
||||
```typescript
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import { getTrigger } from '@/triggers'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
export const {Service}Block: BlockConfig = {
|
||||
type: '{service}',
|
||||
name: '{Service}',
|
||||
// ... other config ...
|
||||
|
||||
// Enable triggers and list available trigger IDs
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: [
|
||||
'{service}_event_a',
|
||||
'{service}_event_b',
|
||||
'{service}_event_c',
|
||||
'{service}_webhook',
|
||||
],
|
||||
},
|
||||
|
||||
subBlocks: [
|
||||
// Regular tool subBlocks first
|
||||
{ id: 'operation', /* ... */ },
|
||||
{ id: 'credential', /* ... */ },
|
||||
// ... other tool fields ...
|
||||
|
||||
// Then spread ALL trigger subBlocks
|
||||
...getTrigger('{service}_event_a').subBlocks,
|
||||
...getTrigger('{service}_event_b').subBlocks,
|
||||
...getTrigger('{service}_event_c').subBlocks,
|
||||
...getTrigger('{service}_webhook').subBlocks,
|
||||
],
|
||||
|
||||
// ... tools config ...
|
||||
}
|
||||
```
|
||||
|
||||
## Automatic Webhook Registration (Preferred)
|
||||
|
||||
If the service's API supports programmatic webhook creation, implement automatic webhook registration instead of requiring users to manually configure webhooks. This provides a much better user experience.
|
||||
|
||||
### When to Use Automatic Registration
|
||||
|
||||
Check the service's API documentation for endpoints like:
|
||||
- `POST /webhooks` or `POST /hooks` - Create webhook
|
||||
- `DELETE /webhooks/{id}` - Delete webhook
|
||||
|
||||
Services that support this pattern include: Grain, Lemlist, Calendly, Airtable, Webflow, Typeform, etc.
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
#### 1. Add API Key to Extra Fields
|
||||
|
||||
Update your `build{Service}ExtraFields` function to include an API key field:
|
||||
|
||||
```typescript
|
||||
export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] {
|
||||
return [
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your {Service} API key',
|
||||
description: 'Required to create the webhook in {Service}.',
|
||||
password: true,
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: { field: 'selectedTriggerId', value: triggerId },
|
||||
},
|
||||
// Other optional fields (e.g., campaign filter, project filter)
|
||||
{
|
||||
id: 'projectId',
|
||||
title: 'Project ID (Optional)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Leave empty for all projects',
|
||||
mode: 'trigger',
|
||||
condition: { field: 'selectedTriggerId', value: triggerId },
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Update Setup Instructions for Automatic Creation
|
||||
|
||||
Change instructions to indicate automatic webhook creation:
|
||||
|
||||
```typescript
|
||||
export function {service}SetupInstructions(eventType: string): string {
|
||||
const instructions = [
|
||||
'Enter your {Service} API Key above.',
|
||||
'You can find your API key in {Service} at <strong>Settings > API</strong>.',
|
||||
`Click <strong>"Save Configuration"</strong> to automatically create the webhook in {Service} for <strong>${eventType}</strong> events.`,
|
||||
'The webhook will be automatically deleted when you remove this trigger.',
|
||||
]
|
||||
|
||||
return instructions
|
||||
.map((instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Add Webhook Creation to API Route
|
||||
|
||||
In `apps/sim/app/api/webhooks/route.ts`, add provider-specific logic after the database save:
|
||||
|
||||
```typescript
|
||||
// --- {Service} specific logic ---
|
||||
if (savedWebhook && provider === '{service}') {
|
||||
logger.info(`[${requestId}] {Service} provider detected. Creating webhook subscription.`)
|
||||
try {
|
||||
const result = await create{Service}WebhookSubscription(
|
||||
{
|
||||
id: savedWebhook.id,
|
||||
path: savedWebhook.path,
|
||||
providerConfig: savedWebhook.providerConfig,
|
||||
},
|
||||
requestId
|
||||
)
|
||||
|
||||
if (result) {
|
||||
// Update the webhook record with the external webhook ID
|
||||
const updatedConfig = {
|
||||
...(savedWebhook.providerConfig as Record<string, any>),
|
||||
externalId: result.id,
|
||||
}
|
||||
await db
|
||||
.update(webhook)
|
||||
.set({
|
||||
providerConfig: updatedConfig,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(webhook.id, savedWebhook.id))
|
||||
|
||||
savedWebhook.providerConfig = updatedConfig
|
||||
logger.info(`[${requestId}] Successfully created {Service} webhook`, {
|
||||
externalHookId: result.id,
|
||||
webhookId: savedWebhook.id,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[${requestId}] Error creating {Service} webhook subscription, rolling back webhook`,
|
||||
err
|
||||
)
|
||||
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to create webhook in {Service}',
|
||||
details: err instanceof Error ? err.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
// --- End {Service} specific logic ---
|
||||
```
|
||||
|
||||
Then add the helper function at the end of the file:
|
||||
|
||||
```typescript
|
||||
async function create{Service}WebhookSubscription(
|
||||
webhookData: any,
|
||||
requestId: string
|
||||
): Promise<{ id: string } | undefined> {
|
||||
try {
|
||||
const { path, providerConfig } = webhookData
|
||||
const { apiKey, triggerId, projectId } = providerConfig || {}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('{Service} API Key is required.')
|
||||
}
|
||||
|
||||
// Map trigger IDs to service event types
|
||||
const eventTypeMap: Record<string, string | undefined> = {
|
||||
{service}_event_a: 'eventA',
|
||||
{service}_event_b: 'eventB',
|
||||
{service}_webhook: undefined, // Generic - no filter
|
||||
}
|
||||
|
||||
const eventType = eventTypeMap[triggerId]
|
||||
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
url: notificationUrl,
|
||||
}
|
||||
|
||||
if (eventType) {
|
||||
requestBody.eventType = eventType
|
||||
}
|
||||
|
||||
if (projectId) {
|
||||
requestBody.projectId = projectId
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.{service}.com/webhooks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
const responseBody = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = responseBody.message || 'Unknown API error'
|
||||
let userFriendlyMessage = 'Failed to create webhook in {Service}'
|
||||
|
||||
if (response.status === 401) {
|
||||
userFriendlyMessage = 'Invalid API Key. Please verify and try again.'
|
||||
} else if (errorMessage) {
|
||||
userFriendlyMessage = `{Service} error: ${errorMessage}`
|
||||
}
|
||||
|
||||
throw new Error(userFriendlyMessage)
|
||||
}
|
||||
|
||||
return { id: responseBody.id }
|
||||
} catch (error: any) {
|
||||
logger.error(`Exception during {Service} webhook creation`, { error: error.message })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Add Webhook Deletion to Provider Subscriptions
|
||||
|
||||
In `apps/sim/lib/webhooks/provider-subscriptions.ts`:
|
||||
|
||||
1. Add a logger:
|
||||
```typescript
|
||||
const {service}Logger = createLogger('{Service}Webhook')
|
||||
```
|
||||
|
||||
2. Add the delete function:
|
||||
```typescript
|
||||
export async function delete{Service}Webhook(webhook: any, requestId: string): Promise<void> {
|
||||
try {
|
||||
const config = getProviderConfig(webhook)
|
||||
const apiKey = config.apiKey as string | undefined
|
||||
const externalId = config.externalId as string | undefined
|
||||
|
||||
if (!apiKey || !externalId) {
|
||||
{service}Logger.warn(`[${requestId}] Missing apiKey or externalId, skipping cleanup`)
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetch(`https://api.{service}.com/webhooks/${externalId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok && response.status !== 404) {
|
||||
{service}Logger.warn(`[${requestId}] Failed to delete webhook (non-fatal): ${response.status}`)
|
||||
} else {
|
||||
{service}Logger.info(`[${requestId}] Successfully deleted webhook ${externalId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
{service}Logger.warn(`[${requestId}] Error deleting webhook (non-fatal)`, error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Add to `cleanupExternalWebhook`:
|
||||
```typescript
|
||||
export async function cleanupExternalWebhook(...): Promise<void> {
|
||||
// ... existing providers ...
|
||||
} else if (webhook.provider === '{service}') {
|
||||
await delete{Service}Webhook(webhook, requestId)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Points for Automatic Registration
|
||||
|
||||
- **API Key visibility**: Always use `password: true` for API key fields
|
||||
- **Error handling**: Roll back the database webhook if external creation fails
|
||||
- **External ID storage**: Save the external webhook ID in `providerConfig.externalId`
|
||||
- **Graceful cleanup**: Don't fail webhook deletion if cleanup fails (use non-fatal logging)
|
||||
- **User-friendly errors**: Map HTTP status codes to helpful error messages
|
||||
|
||||
## The buildTriggerSubBlocks Helper
|
||||
|
||||
This is the generic helper from `@/triggers` that creates consistent trigger subBlocks.
|
||||
|
||||
### Function Signature
|
||||
|
||||
```typescript
|
||||
interface BuildTriggerSubBlocksOptions {
|
||||
triggerId: string // e.g., 'service_event_a'
|
||||
triggerOptions: Array<{ label: string; id: string }> // Dropdown options
|
||||
includeDropdown?: boolean // true only for primary trigger
|
||||
setupInstructions: string // HTML instructions
|
||||
extraFields?: SubBlockConfig[] // Service-specific fields
|
||||
webhookPlaceholder?: string // Custom placeholder text
|
||||
}
|
||||
|
||||
function buildTriggerSubBlocks(options: BuildTriggerSubBlocksOptions): SubBlockConfig[]
|
||||
```
|
||||
|
||||
### What It Creates
|
||||
|
||||
The helper creates this structure:
|
||||
1. **Dropdown** (only if `includeDropdown: true`) - Trigger type selector
|
||||
2. **Webhook URL** - Read-only field with copy button
|
||||
3. **Extra Fields** - Your service-specific fields (filters, options, etc.)
|
||||
4. **Save Button** - Activates the trigger
|
||||
5. **Instructions** - Setup guide for users
|
||||
|
||||
All fields automatically have:
|
||||
- `mode: 'trigger'` - Only shown in trigger mode
|
||||
- `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected
|
||||
|
||||
## Trigger Outputs & Webhook Input Formatting
|
||||
|
||||
### Important: Two Sources of Truth
|
||||
|
||||
There are two related but separate concerns:
|
||||
|
||||
1. **Trigger `outputs`** - Schema/contract defining what fields SHOULD be available. Used by UI for tag dropdown.
|
||||
2. **`formatWebhookInput`** - Implementation that transforms raw webhook payload into actual data. Located in `apps/sim/lib/webhooks/utils.server.ts`.
|
||||
|
||||
**These MUST be aligned.** The fields returned by `formatWebhookInput` should match what's defined in trigger `outputs`. If they differ:
|
||||
- Tag dropdown shows fields that don't exist (broken variable resolution)
|
||||
- Or actual data has fields not shown in dropdown (users can't discover them)
|
||||
|
||||
### When to Add a formatWebhookInput Handler
|
||||
|
||||
- **Simple providers**: If the raw webhook payload structure already matches your outputs, you don't need a handler. The generic fallback returns `body` directly.
|
||||
- **Complex providers**: If you need to transform, flatten, extract nested data, compute fields, or handle conditional logic, add a handler.
|
||||
|
||||
### Adding a Handler
|
||||
|
||||
In `apps/sim/lib/webhooks/utils.server.ts`, add a handler block:
|
||||
|
||||
```typescript
|
||||
if (foundWebhook.provider === '{service}') {
|
||||
// Transform raw webhook body to match trigger outputs
|
||||
return {
|
||||
eventType: body.type,
|
||||
resourceId: body.data?.id || '',
|
||||
timestamp: body.created_at,
|
||||
resource: body.data,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key rules:**
|
||||
- Return fields that match your trigger `outputs` definition exactly
|
||||
- No wrapper objects like `webhook: { data: ... }` or `{service}: { ... }`
|
||||
- No duplication (don't spread body AND add individual fields)
|
||||
- Use `null` for missing optional data, not empty objects with empty strings
|
||||
|
||||
### Verify Alignment
|
||||
|
||||
Run the alignment checker:
|
||||
```bash
|
||||
bunx scripts/check-trigger-alignment.ts {service}
|
||||
```
|
||||
|
||||
## Trigger Outputs
|
||||
|
||||
Trigger outputs use the same schema as block outputs (NOT tool outputs).
|
||||
|
||||
**Supported:**
|
||||
- `type` and `description` for simple fields
|
||||
- Nested object structure for complex data
|
||||
|
||||
**NOT Supported:**
|
||||
- `optional: true` (tool outputs only)
|
||||
- `items` property (tool outputs only)
|
||||
|
||||
```typescript
|
||||
export function buildOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
// Simple fields
|
||||
eventType: { type: 'string', description: 'Event type' },
|
||||
timestamp: { type: 'string', description: 'When it occurred' },
|
||||
|
||||
// Complex data - use type: 'json'
|
||||
payload: { type: 'json', description: 'Full event payload' },
|
||||
|
||||
// Nested structure
|
||||
resource: {
|
||||
id: { type: 'string', description: 'Resource ID' },
|
||||
name: { type: 'string', description: 'Resource name' },
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Generic Webhook Trigger Pattern
|
||||
|
||||
For services with many event types, create a generic webhook that accepts all events:
|
||||
|
||||
```typescript
|
||||
export const {service}WebhookTrigger: TriggerConfig = {
|
||||
id: '{service}_webhook',
|
||||
name: '{Service} Webhook (All Events)',
|
||||
// ...
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: '{service}_webhook',
|
||||
triggerOptions: {service}TriggerOptions,
|
||||
setupInstructions: {service}SetupInstructions('All Events'),
|
||||
extraFields: [
|
||||
// Event type filter (optional)
|
||||
{
|
||||
id: 'eventTypes',
|
||||
title: 'Event Types',
|
||||
type: 'dropdown',
|
||||
multiSelect: true,
|
||||
options: [
|
||||
{ label: 'Event A', id: 'event_a' },
|
||||
{ label: 'Event B', id: 'event_b' },
|
||||
],
|
||||
placeholder: 'Leave empty for all events',
|
||||
mode: 'trigger',
|
||||
condition: { field: 'selectedTriggerId', value: '{service}_webhook' },
|
||||
},
|
||||
// Plus any other service-specific fields
|
||||
...build{Service}ExtraFields('{service}_webhook'),
|
||||
],
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist Before Finishing
|
||||
|
||||
### Utils
|
||||
- [ ] Created `{service}TriggerOptions` array with all trigger IDs
|
||||
- [ ] Created `{service}SetupInstructions` function with clear steps
|
||||
- [ ] Created `build{Service}ExtraFields` for service-specific fields
|
||||
- [ ] Created output builders for each trigger type
|
||||
|
||||
### Triggers
|
||||
- [ ] Primary trigger has `includeDropdown: true`
|
||||
- [ ] Secondary triggers do NOT have `includeDropdown`
|
||||
- [ ] All triggers use `buildTriggerSubBlocks` helper
|
||||
- [ ] All triggers have proper outputs defined
|
||||
- [ ] Created `index.ts` barrel export
|
||||
|
||||
### Registration
|
||||
- [ ] All triggers imported in `triggers/registry.ts`
|
||||
- [ ] All triggers added to `TRIGGER_REGISTRY`
|
||||
- [ ] Block has `triggers.enabled: true`
|
||||
- [ ] Block has all trigger IDs in `triggers.available`
|
||||
- [ ] Block spreads all trigger subBlocks: `...getTrigger('id').subBlocks`
|
||||
|
||||
### Automatic Webhook Registration (if supported)
|
||||
- [ ] Added API key field to `build{Service}ExtraFields` with `password: true`
|
||||
- [ ] Updated setup instructions for automatic webhook creation
|
||||
- [ ] Added provider-specific logic to `apps/sim/app/api/webhooks/route.ts`
|
||||
- [ ] Added `create{Service}WebhookSubscription` helper function
|
||||
- [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts`
|
||||
- [ ] Added provider to `cleanupExternalWebhook` function
|
||||
|
||||
### Webhook Input Formatting
|
||||
- [ ] Added handler in `apps/sim/lib/webhooks/utils.server.ts` (if custom formatting needed)
|
||||
- [ ] Handler returns fields matching trigger `outputs` exactly
|
||||
- [ ] Run `bunx scripts/check-trigger-alignment.ts {service}` to verify alignment
|
||||
|
||||
### Testing
|
||||
- [ ] Run `bun run type-check` to verify no TypeScript errors
|
||||
- [ ] Restart dev server to pick up new triggers
|
||||
- [ ] Test trigger UI shows correctly in the block
|
||||
- [ ] Test automatic webhook creation works (if applicable)
|
||||
5
.agents/skills/add-trigger/agents/openai.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Add Trigger"
|
||||
short_description: "Build Sim webhook triggers"
|
||||
brand_color: "#DC2626"
|
||||
default_prompt: "Use $add-trigger to create or update webhook triggers for a Sim integration."
|
||||
333
.agents/skills/validate-connector/SKILL.md
Normal file
@@ -0,0 +1,333 @@
|
||||
---
|
||||
name: validate-connector
|
||||
description: Audit an existing Sim knowledge base connector against the service API docs and repository conventions, then report and fix issues in auth, config fields, pagination, document mapping, tags, and registry entries. Use when validating or repairing code in `apps/sim/connectors/{service}/`.
|
||||
---
|
||||
|
||||
# Validate Connector Skill
|
||||
|
||||
You are an expert auditor for Sim knowledge base connectors. Your job is to thoroughly validate that an existing connector is correct, complete, and follows all conventions.
|
||||
|
||||
## Your Task
|
||||
|
||||
When the user asks you to validate a connector:
|
||||
1. Read the service's API documentation (via Context7 or WebFetch)
|
||||
2. Read the connector implementation, OAuth config, 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 connector — do not skip any:
|
||||
|
||||
```
|
||||
apps/sim/connectors/{service}/{service}.ts # Connector implementation
|
||||
apps/sim/connectors/{service}/index.ts # Barrel export
|
||||
apps/sim/connectors/registry.ts # Connector registry entry
|
||||
apps/sim/connectors/types.ts # ConnectorConfig interface, ExternalDocument, etc.
|
||||
apps/sim/connectors/utils.ts # Shared utilities (computeContentHash, htmlToPlainText, etc.)
|
||||
apps/sim/lib/oauth/oauth.ts # OAUTH_PROVIDERS — single source of truth for scopes
|
||||
apps/sim/lib/oauth/utils.ts # getCanonicalScopesForProvider, getScopesForService, SCOPE_DESCRIPTIONS
|
||||
apps/sim/lib/oauth/types.ts # OAuthService union type
|
||||
apps/sim/components/icons.tsx # Icon definition for the service
|
||||
```
|
||||
|
||||
If the connector uses selectors, also read:
|
||||
```
|
||||
apps/sim/hooks/selectors/registry.ts # Selector key definitions
|
||||
apps/sim/hooks/selectors/types.ts # SelectorKey union type
|
||||
apps/sim/lib/workflows/subblocks/context.ts # SELECTOR_CONTEXT_FIELDS
|
||||
```
|
||||
|
||||
## 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 (cursor, offset, next token)
|
||||
- Rate limits and error formats
|
||||
- OAuth scopes and their meanings
|
||||
|
||||
Use Context7 (resolve-library-id → query-docs) or WebFetch to retrieve documentation. If both fail, note which claims are based on training knowledge vs verified docs.
|
||||
|
||||
## Step 3: Validate API Endpoints
|
||||
|
||||
For **every** API call in the connector (`listDocuments`, `getDocument`, `validateConfig`, and any helper functions), verify against the API docs:
|
||||
|
||||
### URLs and Methods
|
||||
- [ ] Base URL is correct for the service's API version
|
||||
- [ ] Endpoint paths match the API docs exactly
|
||||
- [ ] HTTP method is correct (GET, POST, PUT, PATCH, DELETE)
|
||||
- [ ] Path parameters are correctly interpolated and URI-encoded where needed
|
||||
- [ ] Query parameters use correct names and formats per the API docs
|
||||
|
||||
### Headers
|
||||
- [ ] Authorization header uses the correct format:
|
||||
- OAuth: `Authorization: Bearer ${accessToken}`
|
||||
- API Key: correct header name per the service's docs
|
||||
- [ ] `Content-Type` is set for POST/PUT/PATCH requests
|
||||
- [ ] Any service-specific headers are present (e.g., `Notion-Version`, `Dropbox-API-Arg`)
|
||||
- [ ] No headers are sent that the API doesn't support or silently ignores
|
||||
|
||||
### Request Bodies
|
||||
- [ ] POST/PUT body fields match API parameter names exactly
|
||||
- [ ] Required fields are always sent
|
||||
- [ ] Optional fields are conditionally included (not sent as `null` or empty unless the API expects that)
|
||||
- [ ] Field value types match API expectations (string vs number vs boolean)
|
||||
|
||||
### Input Sanitization
|
||||
- [ ] User-controlled values interpolated into query strings are properly escaped:
|
||||
- OData `$filter`: single quotes escaped with `''` (e.g., `externalId.replace(/'/g, "''")`)
|
||||
- SOQL: single quotes escaped with `\'`
|
||||
- GraphQL variables: passed as variables, not interpolated into query strings
|
||||
- URL path segments: `encodeURIComponent()` applied
|
||||
- [ ] URL-type config fields (e.g., `siteUrl`, `instanceUrl`) are normalized:
|
||||
- Strip `https://` / `http://` prefix if the API expects bare domains
|
||||
- Strip trailing `/`
|
||||
- Apply `.trim()` before validation
|
||||
|
||||
### Response Parsing
|
||||
- [ ] Response structure is correctly traversed (e.g., `data.results` vs `data.items` vs `data`)
|
||||
- [ ] Field names extracted match what the API actually returns
|
||||
- [ ] Nullable fields are handled with `?? null` or `|| undefined`
|
||||
- [ ] Error responses are checked before accessing data fields
|
||||
|
||||
## Step 4: Validate OAuth Scopes (if OAuth connector)
|
||||
|
||||
Scopes must be correctly declared and sufficient for all API calls the connector makes.
|
||||
|
||||
### Connector requiredScopes
|
||||
- [ ] `requiredScopes` in the connector's `auth` config lists all scopes needed by the connector
|
||||
- [ ] Each scope in `requiredScopes` is a real, valid scope recognized by the service's API
|
||||
- [ ] No invalid, deprecated, or made-up scopes are listed
|
||||
- [ ] No unnecessary excess scopes beyond what the connector actually needs
|
||||
|
||||
### Scope Subset Validation (CRITICAL)
|
||||
- [ ] Every scope in `requiredScopes` exists in the OAuth provider's `scopes` array in `lib/oauth/oauth.ts`
|
||||
- [ ] Find the provider in `OAUTH_PROVIDERS[providerGroup].services[serviceId].scopes`
|
||||
- [ ] Verify: `requiredScopes` ⊆ `OAUTH_PROVIDERS scopes` (every required scope is present in the provider config)
|
||||
- [ ] If a required scope is NOT in the provider config, flag as **critical** — the connector will fail at runtime
|
||||
|
||||
### Scope Sufficiency
|
||||
For each API endpoint the connector calls:
|
||||
- [ ] Identify which scopes are required per the API docs
|
||||
- [ ] Verify those scopes are included in the connector's `requiredScopes`
|
||||
- [ ] If the connector calls endpoints requiring scopes not in `requiredScopes`, flag as **warning**
|
||||
|
||||
### Token Refresh Config
|
||||
- [ ] Check the `getOAuthTokenRefreshConfig` function in `lib/oauth/oauth.ts` for this provider
|
||||
- [ ] `useBasicAuth` matches the service's token exchange requirements
|
||||
- [ ] `supportsRefreshTokenRotation` matches whether the service issues rotating refresh tokens
|
||||
- [ ] Token endpoint URL is correct
|
||||
|
||||
## Step 5: Validate Pagination
|
||||
|
||||
### listDocuments Pagination
|
||||
- [ ] Cursor/pagination parameter name matches the API docs
|
||||
- [ ] Response pagination field is correctly extracted (e.g., `next_cursor`, `nextPageToken`, `@odata.nextLink`, `offset`)
|
||||
- [ ] `hasMore` is correctly determined from the response
|
||||
- [ ] `nextCursor` is correctly passed back for the next page
|
||||
- [ ] `maxItems` / `maxRecords` cap is correctly applied across pages using `syncContext.totalDocsFetched`
|
||||
- [ ] Page size is within the API's allowed range (not exceeding max page size)
|
||||
- [ ] Last page precision: when a `maxItems` cap exists, the final page request uses `Math.min(PAGE_SIZE, remaining)` to avoid fetching more records than needed
|
||||
- [ ] No off-by-one errors in pagination tracking
|
||||
- [ ] The connector does NOT hit known API pagination limits silently (e.g., HubSpot search 10k cap)
|
||||
|
||||
### Pagination State Across Pages
|
||||
- [ ] `syncContext` is used to cache state across pages (user names, field maps, instance URLs, portal IDs, etc.)
|
||||
- [ ] Cached state in `syncContext` is correctly initialized on first page and reused on subsequent pages
|
||||
|
||||
## Step 6: Validate Data Transformation
|
||||
|
||||
### Content Deferral (CRITICAL)
|
||||
Connectors that require per-document API calls to fetch content (file download, export, blocks fetch) MUST use `contentDeferred: true`. This is the standard pattern for reliability — without it, content downloads during listing can exhaust the sync task's time budget before any documents are saved.
|
||||
|
||||
- [ ] If the connector downloads content per-doc during `listDocuments`, it MUST use `contentDeferred: true` instead
|
||||
- [ ] `listDocuments` returns lightweight stubs with `content: ''` and `contentDeferred: true`
|
||||
- [ ] `getDocument` fetches actual content and returns the full document with `contentDeferred: false`
|
||||
- [ ] A shared stub function (e.g., `fileToStub`) is used by both `listDocuments` and `getDocument` to guarantee `contentHash` consistency
|
||||
- [ ] `contentHash` is metadata-based (e.g., `service:{id}:{modifiedTime}`), NOT content-based — it must be derivable from list metadata alone
|
||||
- [ ] The `contentHash` is identical whether produced by `listDocuments` or `getDocument`
|
||||
|
||||
Connectors where the list API already returns content inline (e.g., Slack messages, Reddit posts) do NOT need `contentDeferred`.
|
||||
|
||||
### ExternalDocument Construction
|
||||
- [ ] `externalId` is a stable, unique identifier from the source API
|
||||
- [ ] `title` is extracted from the correct field and has a sensible fallback (e.g., `'Untitled'`)
|
||||
- [ ] `content` is plain text — HTML content is stripped using `htmlToPlainText` from `@/connectors/utils`
|
||||
- [ ] `mimeType` is `'text/plain'`
|
||||
- [ ] `contentHash` uses a metadata-based format (e.g., `service:{id}:{modifiedTime}`) for connectors with `contentDeferred: true`, or `computeContentHash` from `@/connectors/utils` for inline-content connectors
|
||||
- [ ] `sourceUrl` is a valid, complete URL back to the original resource (not relative)
|
||||
- [ ] `metadata` contains all fields referenced by `mapTags` and `tagDefinitions`
|
||||
|
||||
### Content Extraction
|
||||
- [ ] Rich text / HTML fields are converted to plain text before indexing
|
||||
- [ ] Important content is not silently dropped (e.g., nested blocks, table cells, code blocks)
|
||||
- [ ] Content is not silently truncated without logging a warning
|
||||
- [ ] Empty/blank documents are properly filtered out
|
||||
- [ ] Size checks use `Buffer.byteLength(text, 'utf8')` not `text.length` when comparing against byte-based limits (e.g., `MAX_FILE_SIZE` in bytes)
|
||||
|
||||
## Step 7: Validate Tag Definitions and mapTags
|
||||
|
||||
### tagDefinitions
|
||||
- [ ] Each `tagDefinition` has an `id`, `displayName`, and `fieldType`
|
||||
- [ ] `fieldType` matches the actual data type: `'text'` for strings, `'number'` for numbers, `'date'` for dates, `'boolean'` for booleans
|
||||
- [ ] Every `id` in `tagDefinitions` is returned by `mapTags`
|
||||
- [ ] No `tagDefinition` references a field that `mapTags` never produces
|
||||
|
||||
### mapTags
|
||||
- [ ] Return keys match `tagDefinition` `id` values exactly
|
||||
- [ ] Date values are properly parsed using `parseTagDate` from `@/connectors/utils`
|
||||
- [ ] Array values are properly joined using `joinTagArray` from `@/connectors/utils`
|
||||
- [ ] Number values are validated (not `NaN`)
|
||||
- [ ] Metadata field names accessed in `mapTags` match what `listDocuments`/`getDocument` store in `metadata`
|
||||
|
||||
## Step 8: Validate Config Fields and Validation
|
||||
|
||||
### configFields
|
||||
- [ ] Every field has `id`, `title`, `type`
|
||||
- [ ] `required` is set explicitly (not omitted)
|
||||
- [ ] Dropdown fields have `options` with `label` and `id` for each option
|
||||
- [ ] Selector fields follow the canonical pair pattern:
|
||||
- A `type: 'selector'` field with `selectorKey`, `canonicalParamId`, `mode: 'basic'`
|
||||
- A `type: 'short-input'` field with the same `canonicalParamId`, `mode: 'advanced'`
|
||||
- `required` is identical on both fields in the pair
|
||||
- [ ] `selectorKey` values exist in the selector registry
|
||||
- [ ] `dependsOn` references selector field `id` values, not `canonicalParamId`
|
||||
|
||||
### validateConfig
|
||||
- [ ] Validates all required fields are present before making API calls
|
||||
- [ ] Validates optional numeric fields (checks `Number.isNaN`, positive values)
|
||||
- [ ] Makes a lightweight API call to verify access (e.g., fetch 1 record, get profile)
|
||||
- [ ] Uses `VALIDATE_RETRY_OPTIONS` for retry budget
|
||||
- [ ] Returns `{ valid: true }` on success
|
||||
- [ ] Returns `{ valid: false, error: 'descriptive message' }` on failure
|
||||
- [ ] Catches exceptions and returns user-friendly error messages
|
||||
- [ ] Does NOT make expensive calls (full data listing, large queries)
|
||||
|
||||
## Step 9: Validate getDocument
|
||||
|
||||
- [ ] Fetches a single document by `externalId`
|
||||
- [ ] Returns `null` for 404 / not found (does not throw)
|
||||
- [ ] Returns the same `ExternalDocument` shape as `listDocuments`
|
||||
- [ ] If `listDocuments` uses `contentDeferred: true`, `getDocument` MUST fetch actual content and return `contentDeferred: false`
|
||||
- [ ] If `listDocuments` uses `contentDeferred: true`, `getDocument` MUST use the same stub function to ensure `contentHash` is identical
|
||||
- [ ] Handles all content types that `listDocuments` can produce (e.g., if `listDocuments` returns both pages and blogposts, `getDocument` must handle both — not hardcode one endpoint)
|
||||
- [ ] Forwards `syncContext` if it needs cached state (user names, field maps, etc.)
|
||||
- [ ] Error handling is graceful (catches, logs, returns null or throws with context)
|
||||
- [ ] Does not redundantly re-fetch data already included in the initial API response (e.g., if comments come back with the post, don't fetch them again separately)
|
||||
|
||||
## Step 10: Validate General Quality
|
||||
|
||||
### fetchWithRetry Usage
|
||||
- [ ] All external API calls use `fetchWithRetry` from `@/lib/knowledge/documents/utils`
|
||||
- [ ] No raw `fetch()` calls to external APIs
|
||||
- [ ] `VALIDATE_RETRY_OPTIONS` used in `validateConfig`
|
||||
- [ ] If `validateConfig` calls a shared helper (e.g., `linearGraphQL`, `resolveId`), that helper must accept and forward `retryOptions` to `fetchWithRetry`
|
||||
- [ ] Default retry options used in `listDocuments`/`getDocument`
|
||||
|
||||
### API Efficiency
|
||||
- [ ] APIs that support field selection (e.g., `$select`, `sysparm_fields`, `fields`) should request only the fields the connector needs — in both `listDocuments` AND `getDocument`
|
||||
- [ ] No redundant API calls: if a helper already fetches data (e.g., site metadata), callers should reuse the result instead of making a second call for the same information
|
||||
- [ ] Sequential per-item API calls (fetching details for each document in a loop) should be batched with `Promise.all` and a concurrency limit of 3-5
|
||||
|
||||
### Error Handling
|
||||
- [ ] Individual document failures are caught and logged without aborting the sync
|
||||
- [ ] API error responses include status codes in error messages
|
||||
- [ ] No unhandled promise rejections in concurrent operations
|
||||
|
||||
### Concurrency
|
||||
- [ ] Concurrent API calls use reasonable batch sizes (3-5 is typical)
|
||||
- [ ] No unbounded `Promise.all` over large arrays
|
||||
|
||||
### Logging
|
||||
- [ ] Uses `createLogger` from `@sim/logger` (not `console.log`)
|
||||
- [ ] Logs sync progress at `info` level
|
||||
- [ ] Logs errors at `warn` or `error` level with context
|
||||
|
||||
### Registry
|
||||
- [ ] Connector is exported from `connectors/{service}/index.ts`
|
||||
- [ ] Connector is registered in `connectors/registry.ts`
|
||||
- [ ] Registry key matches the connector's `id` field
|
||||
|
||||
## Step 11: Report and Fix
|
||||
|
||||
### Report Format
|
||||
|
||||
Group findings by severity:
|
||||
|
||||
**Critical** (will cause runtime errors, data loss, or auth failures):
|
||||
- Wrong API endpoint URL or HTTP method
|
||||
- Invalid or missing OAuth scopes (not in provider config)
|
||||
- Incorrect response field mapping (accessing wrong path)
|
||||
- SOQL/query fields that don't exist on the target object
|
||||
- Pagination that silently hits undocumented API limits
|
||||
- Missing error handling that would crash the sync
|
||||
- `requiredScopes` not a subset of OAuth provider scopes
|
||||
- Query/filter injection: user-controlled values interpolated into OData `$filter`, SOQL, or query strings without escaping
|
||||
- Per-document content download in `listDocuments` without `contentDeferred: true` — causes sync timeouts for large document sets
|
||||
- `contentHash` mismatch between `listDocuments` stub and `getDocument` return — causes unnecessary re-processing every sync
|
||||
|
||||
**Warning** (incorrect behavior, data quality issues, or convention violations):
|
||||
- HTML content not stripped via `htmlToPlainText`
|
||||
- `getDocument` not forwarding `syncContext`
|
||||
- `getDocument` hardcoded to one content type when `listDocuments` returns multiple (e.g., only pages but not blogposts)
|
||||
- Missing `tagDefinition` for metadata fields returned by `mapTags`
|
||||
- Incorrect `useBasicAuth` or `supportsRefreshTokenRotation` in token refresh config
|
||||
- Invalid scope names that the API doesn't recognize (even if silently ignored)
|
||||
- Private resources excluded from name-based lookup despite scopes being available
|
||||
- Silent data truncation without logging
|
||||
- Size checks using `text.length` (character count) instead of `Buffer.byteLength` (byte count) for byte-based limits
|
||||
- URL-type config fields not normalized (protocol prefix, trailing slashes cause API failures)
|
||||
- `VALIDATE_RETRY_OPTIONS` not threaded through helper functions called by `validateConfig`
|
||||
|
||||
**Suggestion** (minor improvements):
|
||||
- Missing incremental sync support despite API supporting it
|
||||
- Overly broad scopes that could be narrowed (not wrong, but could be tighter)
|
||||
- Source URL format could be more specific
|
||||
- Missing `orderBy` for deterministic pagination
|
||||
- Redundant API calls that could be cached in `syncContext`
|
||||
- Sequential per-item API calls that could be batched with `Promise.all` (concurrency 3-5)
|
||||
- API supports field selection but connector fetches all fields (e.g., missing `$select`, `sysparm_fields`, `fields`)
|
||||
- `getDocument` re-fetches data already included in the initial API response (e.g., comments returned with post)
|
||||
- Last page of pagination requests full `PAGE_SIZE` when fewer records remain (`Math.min(PAGE_SIZE, remaining)`)
|
||||
|
||||
### 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
|
||||
2. TypeScript compiles clean
|
||||
3. Re-read all modified files to verify fixes are correct
|
||||
|
||||
## Checklist Summary
|
||||
|
||||
- [ ] Read connector implementation, types, utils, registry, and OAuth config
|
||||
- [ ] Pulled and read official API documentation for the service
|
||||
- [ ] Validated every API endpoint URL, method, headers, and body against API docs
|
||||
- [ ] Validated input sanitization: no query/filter injection, URL fields normalized
|
||||
- [ ] Validated OAuth scopes: `requiredScopes` ⊆ OAuth provider `scopes` in `oauth.ts`
|
||||
- [ ] Validated each scope is real and recognized by the service's API
|
||||
- [ ] Validated scopes are sufficient for all API endpoints the connector calls
|
||||
- [ ] Validated token refresh config (`useBasicAuth`, `supportsRefreshTokenRotation`)
|
||||
- [ ] Validated pagination: cursor names, page sizes, hasMore logic, no silent caps
|
||||
- [ ] Validated content deferral: `contentDeferred: true` used when per-doc content fetch required, metadata-based `contentHash` consistent between stub and `getDocument`
|
||||
- [ ] Validated data transformation: plain text extraction, HTML stripping, content hashing
|
||||
- [ ] Validated tag definitions match mapTags output, correct fieldTypes
|
||||
- [ ] Validated config fields: canonical pairs, selector keys, required flags
|
||||
- [ ] Validated validateConfig: lightweight check, error messages, retry options
|
||||
- [ ] Validated getDocument: null on 404, all content types handled, no redundant re-fetches, syncContext forwarding
|
||||
- [ ] Validated fetchWithRetry used for all external calls (no raw fetch), VALIDATE_RETRY_OPTIONS threaded through helpers
|
||||
- [ ] Validated API efficiency: field selection used, no redundant calls, sequential fetches batched
|
||||
- [ ] Validated error handling: graceful failures, no unhandled rejections
|
||||
- [ ] Validated logging: createLogger, no console.log
|
||||
- [ ] Validated registry: correct export, correct key
|
||||
- [ ] Reported all issues grouped by severity
|
||||
- [ ] Fixed all critical and warning issues
|
||||
- [ ] Ran `bun run lint` after fixes
|
||||
- [ ] Verified TypeScript compiles clean
|
||||
5
.agents/skills/validate-connector/agents/openai.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Validate Connector"
|
||||
short_description: "Audit a Sim knowledge connector"
|
||||
brand_color: "#059669"
|
||||
default_prompt: "Use $validate-connector to audit and fix a Sim knowledge connector against its API docs."
|
||||
289
.agents/skills/validate-integration/SKILL.md
Normal file
@@ -0,0 +1,289 @@
|
||||
---
|
||||
name: validate-integration
|
||||
description: Audit an existing Sim integration against the service API docs and repository conventions, then report and fix issues across tools, blocks, outputs, OAuth scopes, triggers, and registry entries. Use when validating or repairing a service integration under `apps/sim/tools`, `apps/sim/blocks`, or `apps/sim/triggers`.
|
||||
---
|
||||
|
||||
# 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
|
||||
5
.agents/skills/validate-integration/agents/openai.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Validate Integration"
|
||||
short_description: "Audit a Sim service integration"
|
||||
brand_color: "#B45309"
|
||||
default_prompt: "Use $validate-integration to audit and fix a Sim integration against its API docs."
|
||||
@@ -192,7 +192,7 @@ In the block config (`blocks/blocks/{service}.ts`), add `hideWhenHosted: true` t
|
||||
},
|
||||
```
|
||||
|
||||
The visibility is controlled by `isSubBlockHiddenByHostedKey()` in `lib/workflows/subblocks/visibility.ts`, which checks the `isHosted` feature flag.
|
||||
The visibility is controlled by `isSubBlockHidden()` in `lib/workflows/subblocks/visibility.ts`, which checks both the `isHosted` feature flag (`hideWhenHosted`) and optional env var conditions (`hideWhenEnvSet`).
|
||||
|
||||
### Excluding Specific Operations from Hosted Key Support
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM oven/bun:1.3.10-alpine
|
||||
FROM oven/bun:1.3.11-alpine
|
||||
|
||||
# Install necessary packages for development
|
||||
RUN apk add --no-cache \
|
||||
|
||||
14
.github/workflows/ci.yml
vendored
@@ -2,9 +2,9 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, staging]
|
||||
branches: [main, staging, dev]
|
||||
pull_request:
|
||||
branches: [main, staging]
|
||||
branches: [main, staging, dev]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
detect-version:
|
||||
name: Detect Version
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
|
||||
outputs:
|
||||
version: ${{ steps.extract.outputs.version }}
|
||||
is_release: ${{ steps.extract.outputs.is_release }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
build-amd64:
|
||||
name: Build AMD64
|
||||
needs: [test-build, detect-version]
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -75,8 +75,8 @@ jobs:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
@@ -109,6 +109,8 @@ jobs:
|
||||
# ECR tags (always build for ECR)
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
ECR_TAG="latest"
|
||||
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
|
||||
ECR_TAG="dev"
|
||||
else
|
||||
ECR_TAG="staging"
|
||||
fi
|
||||
|
||||
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.10
|
||||
bun-version: 1.3.11
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
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.10
|
||||
bun-version: 1.3.11
|
||||
|
||||
- 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.10
|
||||
bun-version: 1.3.11
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
|
||||
6
.github/workflows/images.yml
vendored
@@ -36,8 +36,8 @@ jobs:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
@@ -70,6 +70,8 @@ jobs:
|
||||
# ECR tags (always build for ECR)
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
ECR_TAG="latest"
|
||||
elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then
|
||||
ECR_TAG="dev"
|
||||
else
|
||||
ECR_TAG="staging"
|
||||
fi
|
||||
|
||||
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.10
|
||||
bun-version: 1.3.11
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
|
||||
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.10
|
||||
bun-version: 1.3.11
|
||||
|
||||
- name: Setup Node.js for npm publishing
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
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.10
|
||||
bun-version: 1.3.11
|
||||
|
||||
- name: Setup Node.js for npm publishing
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/test-build.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
bun-version: 1.3.11
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
383
AGENTS.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# Sim Development Guidelines
|
||||
|
||||
You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.
|
||||
|
||||
## Global Standards
|
||||
|
||||
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
|
||||
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
|
||||
- **Styling**: Never update global styles. Keep all styling local to components
|
||||
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Principles
|
||||
1. Single Responsibility: Each component, hook, store has one clear purpose
|
||||
2. Composition Over Complexity: Break down complex logic into smaller pieces
|
||||
3. Type Safety First: TypeScript interfaces for all props, state, return types
|
||||
4. Predictable State: Zustand for global state, useState for UI-only concerns
|
||||
|
||||
### Root Structure
|
||||
```
|
||||
apps/sim/
|
||||
├── app/ # Next.js app router (pages, API routes)
|
||||
├── blocks/ # Block definitions and registry
|
||||
├── components/ # Shared UI (emcn/, ui/)
|
||||
├── executor/ # Workflow execution engine
|
||||
├── hooks/ # Shared hooks (queries/, selectors/)
|
||||
├── lib/ # App-wide utilities
|
||||
├── providers/ # LLM provider integrations
|
||||
├── stores/ # Zustand stores
|
||||
├── tools/ # Tool definitions
|
||||
└── triggers/ # Trigger definitions
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
- Components: PascalCase (`WorkflowList`)
|
||||
- Hooks: `use` prefix (`useWorkflowOperations`)
|
||||
- Files: kebab-case (`workflow-list.tsx`)
|
||||
- Stores: `stores/feature/store.ts`
|
||||
- Constants: SCREAMING_SNAKE_CASE
|
||||
- Interfaces: PascalCase with suffix (`WorkflowListProps`)
|
||||
|
||||
## Imports
|
||||
|
||||
**Always use absolute imports.** Never use relative imports.
|
||||
|
||||
```typescript
|
||||
// ✓ Good
|
||||
import { useWorkflowStore } from '@/stores/workflows/store'
|
||||
|
||||
// ✗ Bad
|
||||
import { useWorkflowStore } from '../../../stores/workflows/store'
|
||||
```
|
||||
|
||||
Use barrel exports (`index.ts`) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source.
|
||||
|
||||
### Import Order
|
||||
1. React/core libraries
|
||||
2. External libraries
|
||||
3. UI components (`@/components/emcn`, `@/components/ui`)
|
||||
4. Utilities (`@/lib/...`)
|
||||
5. Stores (`@/stores/...`)
|
||||
6. Feature imports
|
||||
7. CSS imports
|
||||
|
||||
Use `import type { X }` for type-only imports.
|
||||
|
||||
## TypeScript
|
||||
|
||||
1. No `any` - Use proper types or `unknown` with type guards
|
||||
2. Always define props interface for components
|
||||
3. `as const` for constant objects/arrays
|
||||
4. Explicit ref types: `useRef<HTMLDivElement>(null)`
|
||||
|
||||
## Components
|
||||
|
||||
```typescript
|
||||
'use client' // Only if using hooks
|
||||
|
||||
const CONFIG = { SPACING: 8 } as const
|
||||
|
||||
interface ComponentProps {
|
||||
requiredProp: string
|
||||
optionalProp?: boolean
|
||||
}
|
||||
|
||||
export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
|
||||
// Order: refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return
|
||||
}
|
||||
```
|
||||
|
||||
Extract when: 50+ lines, used in 2+ files, or has own state/logic. Keep inline when: < 10 lines, single use, purely presentational.
|
||||
|
||||
## Hooks
|
||||
|
||||
```typescript
|
||||
interface UseFeatureProps { id: string }
|
||||
|
||||
export function useFeature({ id }: UseFeatureProps) {
|
||||
const idRef = useRef(id)
|
||||
const [data, setData] = useState<Data | null>(null)
|
||||
|
||||
useEffect(() => { idRef.current = id }, [id])
|
||||
|
||||
const fetchData = useCallback(async () => { ... }, []) // Empty deps when using refs
|
||||
|
||||
return { data, fetchData }
|
||||
}
|
||||
```
|
||||
|
||||
## Zustand Stores
|
||||
|
||||
Stores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
const initialState = { items: [] as Item[] }
|
||||
|
||||
export const useFeatureStore = create<FeatureState>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
...initialState,
|
||||
setItems: (items) => set({ items }),
|
||||
reset: () => set(initialState),
|
||||
}),
|
||||
{ name: 'feature-store' }
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
Use `devtools` middleware. Use `persist` only when data should survive reload with `partialize` to persist only necessary state.
|
||||
|
||||
## React Query
|
||||
|
||||
All React Query hooks live in `hooks/queries/`. All server state must go through React Query — never use `useState` + `fetch` in components for data fetching or mutations.
|
||||
|
||||
### Query Key Factory
|
||||
|
||||
Every file must have a hierarchical key factory with an `all` root key and intermediate plural keys for prefix invalidation:
|
||||
|
||||
```typescript
|
||||
export const entityKeys = {
|
||||
all: ['entity'] as const,
|
||||
lists: () => [...entityKeys.all, 'list'] as const,
|
||||
list: (workspaceId?: string) => [...entityKeys.lists(), workspaceId ?? ''] as const,
|
||||
details: () => [...entityKeys.all, 'detail'] as const,
|
||||
detail: (id?: string) => [...entityKeys.details(), id ?? ''] as const,
|
||||
}
|
||||
```
|
||||
|
||||
### Query Hooks
|
||||
|
||||
- Every `queryFn` must forward `signal` for request cancellation
|
||||
- Every query must have an explicit `staleTime`
|
||||
- Use `keepPreviousData` only on variable-key queries (where params change), never on static keys
|
||||
|
||||
```typescript
|
||||
export function useEntityList(workspaceId?: string) {
|
||||
return useQuery({
|
||||
queryKey: entityKeys.list(workspaceId),
|
||||
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
|
||||
enabled: Boolean(workspaceId),
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData, // OK: workspaceId varies
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Mutation Hooks
|
||||
|
||||
- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible
|
||||
- For optimistic updates: use `onSettled` (not `onSuccess`) for cache reconciliation — `onSettled` fires on both success and error
|
||||
- Don't include mutation objects in `useCallback` deps — `.mutate()` is stable in TanStack Query v5
|
||||
|
||||
```typescript
|
||||
export function useUpdateEntity() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (variables) => { /* ... */ },
|
||||
onMutate: async (variables) => {
|
||||
await queryClient.cancelQueries({ queryKey: entityKeys.detail(variables.id) })
|
||||
const previous = queryClient.getQueryData(entityKeys.detail(variables.id))
|
||||
queryClient.setQueryData(entityKeys.detail(variables.id), /* optimistic */)
|
||||
return { previous }
|
||||
},
|
||||
onError: (_err, variables, context) => {
|
||||
queryClient.setQueryData(entityKeys.detail(variables.id), context?.previous)
|
||||
},
|
||||
onSettled: (_data, _error, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: entityKeys.detail(variables.id) })
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Use Tailwind only, no inline styles. Use `cn()` from `@/lib/utils` for conditional classes.
|
||||
|
||||
```typescript
|
||||
<div className={cn('base-classes', isActive && 'active-classes')} />
|
||||
```
|
||||
|
||||
## EMCN Components
|
||||
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA when 2+ variants exist.
|
||||
|
||||
## Testing
|
||||
|
||||
Use Vitest. Test files: `feature.ts` → `feature.test.ts`. See `.cursor/rules/sim-testing.mdc` for full details.
|
||||
|
||||
### Global Mocks (vitest.setup.ts)
|
||||
|
||||
`@sim/db`, `drizzle-orm`, `@sim/logger`, `@/blocks/registry`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior.
|
||||
|
||||
### Standard Test Pattern
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockGetSession } = vi.hoisted(() => ({
|
||||
mockGetSession: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
import { GET } from '@/app/api/my-route/route'
|
||||
|
||||
describe('my route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
|
||||
})
|
||||
it('returns data', async () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
- Never create `utils.ts` for single consumer - inline it
|
||||
- Create `utils.ts` when 2+ files need the same helper
|
||||
- Check existing sources in `lib/` before duplicating
|
||||
|
||||
## Adding Integrations
|
||||
|
||||
New integrations require: **Tools** → **Block** → **Icon** → (optional) **Trigger**
|
||||
|
||||
Always look up the service's API docs first.
|
||||
|
||||
### 1. Tools (`tools/{service}/`)
|
||||
|
||||
```
|
||||
tools/{service}/
|
||||
├── index.ts # Barrel export
|
||||
├── types.ts # Params/response types
|
||||
└── {action}.ts # Tool implementation
|
||||
```
|
||||
|
||||
**Tool structure:**
|
||||
```typescript
|
||||
export const serviceTool: ToolConfig<Params, Response> = {
|
||||
id: 'service_action',
|
||||
name: 'Service Action',
|
||||
description: '...',
|
||||
version: '1.0.0',
|
||||
oauth: { required: true, provider: 'service' },
|
||||
params: { /* ... */ },
|
||||
request: { url: '/api/tools/service/action', method: 'POST', ... },
|
||||
transformResponse: async (response) => { /* ... */ },
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
Register in `tools/registry.ts`.
|
||||
|
||||
### 2. Block (`blocks/blocks/{service}.ts`)
|
||||
|
||||
```typescript
|
||||
export const ServiceBlock: BlockConfig = {
|
||||
type: 'service',
|
||||
name: 'Service',
|
||||
description: '...',
|
||||
category: 'tools',
|
||||
bgColor: '#hexcolor',
|
||||
icon: ServiceIcon,
|
||||
subBlocks: [ /* see SubBlock Properties */ ],
|
||||
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}`, params: (p) => ({ /* type coercions here */ }) } },
|
||||
inputs: { /* ... */ },
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
Register in `blocks/registry.ts` (alphabetically).
|
||||
|
||||
**Important:** `tools.config.tool` runs during serialization (before variable resolution). Never do `Number()` or other type coercions there — dynamic references like `<Block.output>` will be destroyed. Use `tools.config.params` for type coercions (it runs during execution, after variables are resolved).
|
||||
|
||||
**SubBlock Properties:**
|
||||
```typescript
|
||||
{
|
||||
id: 'field', title: 'Label', type: 'short-input', placeholder: '...',
|
||||
required: true, // or condition object
|
||||
condition: { field: 'op', value: 'send' }, // show/hide
|
||||
dependsOn: ['credential'], // clear when dep changes
|
||||
mode: 'basic', // 'basic' | 'advanced' | 'both' | 'trigger'
|
||||
}
|
||||
```
|
||||
|
||||
**condition examples:**
|
||||
- `{ field: 'op', value: 'send' }` - show when op === 'send'
|
||||
- `{ field: 'op', value: ['a','b'] }` - show when op is 'a' OR 'b'
|
||||
- `{ field: 'op', value: 'x', not: true }` - show when op !== 'x'
|
||||
- `{ field: 'op', value: 'x', not: true, and: { field: 'type', value: 'dm', not: true } }` - complex
|
||||
|
||||
**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }`
|
||||
|
||||
**File Input Pattern (basic/advanced mode):**
|
||||
```typescript
|
||||
// Basic: file-upload UI
|
||||
{ id: 'uploadFile', type: 'file-upload', canonicalParamId: 'file', mode: 'basic' },
|
||||
// Advanced: reference from other blocks
|
||||
{ id: 'fileRef', type: 'short-input', canonicalParamId: 'file', mode: 'advanced' },
|
||||
```
|
||||
|
||||
In `tools.config.tool`, normalize with:
|
||||
```typescript
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
const file = normalizeFileInput(params.uploadFile || params.fileRef, { single: true })
|
||||
if (file) params.file = file
|
||||
```
|
||||
|
||||
For file uploads, create an internal API route (`/api/tools/{service}/upload`) that uses `downloadFileFromStorage` to get file content from `UserFile` objects.
|
||||
|
||||
### 3. Icon (`components/icons.tsx`)
|
||||
|
||||
```typescript
|
||||
export function ServiceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return <svg {...props}>/* SVG from brand assets */</svg>
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Trigger (`triggers/{service}/`) - Optional
|
||||
|
||||
```
|
||||
triggers/{service}/
|
||||
├── index.ts # Barrel export
|
||||
├── webhook.ts # Webhook handler
|
||||
└── {event}.ts # Event-specific handlers
|
||||
```
|
||||
|
||||
Register in `triggers/registry.ts`.
|
||||
|
||||
### Integration Checklist
|
||||
|
||||
- [ ] Look up API docs
|
||||
- [ ] Create `tools/{service}/` with types and tools
|
||||
- [ ] Register tools in `tools/registry.ts`
|
||||
- [ ] Add icon to `components/icons.tsx`
|
||||
- [ ] Create block in `blocks/blocks/{service}.ts`
|
||||
- [ ] Register block in `blocks/registry.ts`
|
||||
- [ ] (Optional) Create and register triggers
|
||||
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
|
||||
- [ ] (If file uploads) Use `normalizeFileInput` in block config
|
||||
12
README.md
@@ -3,7 +3,7 @@
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/sim/public/logo/wordmark.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="apps/sim/public/logo/wordmark-dark.svg">
|
||||
<img src="apps/sim/public/logo/wordmark-dark.svg" alt="Sim Logo" width="300"/>
|
||||
<img src="apps/sim/public/logo/wordmark-dark.svg" alt="Sim Logo" width="380"/>
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
@@ -74,6 +74,10 @@ docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
#### Background worker note
|
||||
|
||||
The Docker Compose stack starts a dedicated worker container by default. If `REDIS_URL` is not configured, the worker will start, log that it is idle, and do no queue processing. This is expected. Queue-backed API, webhook, and schedule execution requires Redis; installs without Redis continue to use the inline execution path.
|
||||
|
||||
Sim also supports local models via [Ollama](https://ollama.ai) and [vLLM](https://docs.vllm.ai/) — see the [Docker self-hosting docs](https://docs.sim.ai/self-hosting/docker) for setup details.
|
||||
|
||||
### Self-hosted: Manual Setup
|
||||
@@ -113,10 +117,12 @@ cd packages/db && bunx drizzle-kit migrate --config=./drizzle.config.ts
|
||||
5. Start development servers:
|
||||
|
||||
```bash
|
||||
bun run dev:full # Starts both Next.js app and realtime socket server
|
||||
bun run dev:full # Starts Next.js app, realtime socket server, and the BullMQ worker
|
||||
```
|
||||
|
||||
Or run separately: `bun run dev` (Next.js) and `cd apps/sim && bun run dev:sockets` (realtime).
|
||||
If `REDIS_URL` is not configured, the worker will remain idle and execution continues inline.
|
||||
|
||||
Or run separately: `bun run dev` (Next.js), `cd apps/sim && bun run dev:sockets` (realtime), and `cd apps/sim && bun run worker` (BullMQ worker).
|
||||
|
||||
## Copilot API Keys
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
themeColor: [
|
||||
{ media: '(prefers-color-scheme: light)', color: '#ffffff' },
|
||||
{ media: '(prefers-color-scheme: dark)', color: '#0c0c0c' },
|
||||
@@ -20,7 +18,7 @@ export const metadata = {
|
||||
metadataBase: new URL('https://docs.sim.ai'),
|
||||
title: {
|
||||
default: 'Sim Documentation — Build AI Agents & Run Your Agentic Workforce',
|
||||
template: '%s',
|
||||
template: '%s | Sim Docs',
|
||||
},
|
||||
description:
|
||||
'Documentation for Sim — the open-source platform to build AI agents and run your agentic workforce. Connect 1,000+ integrations and LLMs to deploy and orchestrate agentic workflows.',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import type { ComponentType, SVGProps } from 'react'
|
||||
import {
|
||||
A2AIcon,
|
||||
AgentMailIcon,
|
||||
AhrefsIcon,
|
||||
AirtableIcon,
|
||||
AirweaveIcon,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
EnrichSoIcon,
|
||||
EvernoteIcon,
|
||||
ExaAIIcon,
|
||||
ExtendIcon,
|
||||
EyeIcon,
|
||||
FathomIcon,
|
||||
FirecrawlIcon,
|
||||
@@ -74,6 +76,7 @@ import {
|
||||
GoogleVaultIcon,
|
||||
GrafanaIcon,
|
||||
GrainIcon,
|
||||
GranolaIcon,
|
||||
GreenhouseIcon,
|
||||
GreptileIcon,
|
||||
HexIcon,
|
||||
@@ -88,7 +91,9 @@ import {
|
||||
JiraIcon,
|
||||
JiraServiceManagementIcon,
|
||||
KalshiIcon,
|
||||
KetchIcon,
|
||||
LangsmithIcon,
|
||||
LaunchDarklyIcon,
|
||||
LemlistIcon,
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
@@ -124,6 +129,7 @@ import {
|
||||
PolymarketIcon,
|
||||
PostgresIcon,
|
||||
PosthogIcon,
|
||||
ProfoundIcon,
|
||||
PulseIcon,
|
||||
QdrantIcon,
|
||||
QuiverIcon,
|
||||
@@ -133,9 +139,12 @@ import {
|
||||
ReductoIcon,
|
||||
ResendIcon,
|
||||
RevenueCatIcon,
|
||||
RipplingIcon,
|
||||
RootlyIcon,
|
||||
S3Icon,
|
||||
SalesforceIcon,
|
||||
SearchIcon,
|
||||
SecretsManagerIcon,
|
||||
SendgridIcon,
|
||||
SentryIcon,
|
||||
SerperIcon,
|
||||
@@ -151,6 +160,7 @@ import {
|
||||
StagehandIcon,
|
||||
StripeIcon,
|
||||
SupabaseIcon,
|
||||
TailscaleIcon,
|
||||
TavilyIcon,
|
||||
TelegramIcon,
|
||||
TextractIcon,
|
||||
@@ -180,6 +190,7 @@ type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
|
||||
export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
a2a: A2AIcon,
|
||||
agentmail: AgentMailIcon,
|
||||
ahrefs: AhrefsIcon,
|
||||
airtable: AirtableIcon,
|
||||
airweave: AirweaveIcon,
|
||||
@@ -217,6 +228,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
enrich: EnrichSoIcon,
|
||||
evernote: EvernoteIcon,
|
||||
exa: ExaAIIcon,
|
||||
extend_v2: ExtendIcon,
|
||||
fathom: FathomIcon,
|
||||
file_v3: DocumentIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
@@ -246,6 +258,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
google_vault: GoogleVaultIcon,
|
||||
grafana: GrafanaIcon,
|
||||
grain: GrainIcon,
|
||||
granola: GranolaIcon,
|
||||
greenhouse: GreenhouseIcon,
|
||||
greptile: GreptileIcon,
|
||||
hex: HexIcon,
|
||||
@@ -261,8 +274,10 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
jira: JiraIcon,
|
||||
jira_service_management: JiraServiceManagementIcon,
|
||||
kalshi_v2: KalshiIcon,
|
||||
ketch: KetchIcon,
|
||||
knowledge: PackageSearchIcon,
|
||||
langsmith: LangsmithIcon,
|
||||
launchdarkly: LaunchDarklyIcon,
|
||||
lemlist: LemlistIcon,
|
||||
linear: LinearIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
@@ -297,6 +312,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
polymarket: PolymarketIcon,
|
||||
postgresql: PostgresIcon,
|
||||
posthog: PosthogIcon,
|
||||
profound: ProfoundIcon,
|
||||
pulse_v2: PulseIcon,
|
||||
qdrant: QdrantIcon,
|
||||
quiver: QuiverIcon,
|
||||
@@ -306,9 +322,12 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
reducto_v2: ReductoIcon,
|
||||
resend: ResendIcon,
|
||||
revenuecat: RevenueCatIcon,
|
||||
rippling: RipplingIcon,
|
||||
rootly: RootlyIcon,
|
||||
s3: S3Icon,
|
||||
salesforce: SalesforceIcon,
|
||||
search: SearchIcon,
|
||||
secrets_manager: SecretsManagerIcon,
|
||||
sendgrid: SendgridIcon,
|
||||
sentry: SentryIcon,
|
||||
serper: SerperIcon,
|
||||
@@ -325,6 +344,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
stripe: StripeIcon,
|
||||
stt_v2: STTIcon,
|
||||
supabase: SupabaseIcon,
|
||||
tailscale: TailscaleIcon,
|
||||
tavily: TavilyIcon,
|
||||
telegram: TelegramIcon,
|
||||
textract_v2: TextractIcon,
|
||||
|
||||
@@ -131,7 +131,7 @@ Erkennt personenbezogene Daten mithilfe von Microsoft Presidio. Unterstützt üb
|
||||
**Anwendungsfälle:**
|
||||
- Blockieren von Inhalten mit sensiblen persönlichen Informationen
|
||||
- Maskieren von personenbezogenen Daten vor der Protokollierung oder Speicherung
|
||||
- Einhaltung der DSGVO, HIPAA und anderer Datenschutzbestimmungen
|
||||
- Einhaltung der DSGVO und anderer Datenschutzbestimmungen
|
||||
- Bereinigung von Benutzereingaben vor der Verarbeitung
|
||||
|
||||
## Konfiguration
|
||||
|
||||
150
apps/docs/content/docs/en/blocks/credential.mdx
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: Credential
|
||||
---
|
||||
|
||||
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 Credential block has two operations: **Select Credential** picks a single OAuth credential and outputs its ID reference for downstream blocks; **List Credentials** returns all OAuth credentials in the workspace (optionally filtered by provider) as an array for iteration.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/credential.png"
|
||||
alt="Credential Block"
|
||||
width={400}
|
||||
height={300}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Callout>
|
||||
The Credential block outputs credential **ID references**, not secrets. Downstream blocks receive the ID and resolve the actual OAuth token securely during their own execution.
|
||||
</Callout>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Operation
|
||||
|
||||
| Value | Description |
|
||||
|---|---|
|
||||
| **Select Credential** | Pick one OAuth credential and output its reference — use this to wire a single credential into downstream blocks |
|
||||
| **List Credentials** | Return all OAuth credentials in the workspace as an array — use this with a ForEach loop |
|
||||
|
||||
### Credential (Select operation)
|
||||
|
||||
Select an OAuth credential from your workspace. The dropdown shows all connected OAuth accounts (Google, GitHub, Slack, etc.).
|
||||
|
||||
In advanced mode, paste a credential ID directly. You can copy a credential ID from your workspace's Credentials settings page.
|
||||
|
||||
### Provider (List operation)
|
||||
|
||||
Filter the returned OAuth credentials by provider. Select one or more providers from the dropdown — only providers you have credentials for will appear. Leave empty to return all OAuth credentials.
|
||||
|
||||
| Example | Returns |
|
||||
|---|---|
|
||||
| Gmail | Gmail credentials only |
|
||||
| Slack | Slack credentials only |
|
||||
| Gmail + Slack | Gmail and Slack credentials |
|
||||
|
||||
## Outputs
|
||||
|
||||
<Tabs items={['Select Credential', 'List Credentials']}>
|
||||
<Tab>
|
||||
| Output | Type | Description |
|
||||
|---|---|---|
|
||||
| `credentialId` | `string` | The credential ID — pipe this into other blocks' credential fields |
|
||||
| `displayName` | `string` | Human-readable name (e.g. "waleed@company.com") |
|
||||
| `providerId` | `string` | OAuth provider ID (e.g. `google-email`, `slack`) |
|
||||
</Tab>
|
||||
<Tab>
|
||||
| Output | Type | Description |
|
||||
|---|---|---|
|
||||
| `credentials` | `json` | Array of OAuth credential objects (see shape below) |
|
||||
| `count` | `number` | Number of credentials returned |
|
||||
|
||||
Each object in the `credentials` array:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `credentialId` | `string` | The credential ID |
|
||||
| `displayName` | `string` | Human-readable name |
|
||||
| `providerId` | `string` | OAuth provider ID |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
**Shared credential across multiple blocks** — Define once, use everywhere
|
||||
```
|
||||
Credential (Select, Google) → Gmail (Send) & Google Drive (Upload) & Google Calendar (Create)
|
||||
```
|
||||
|
||||
**Multi-account workflows** — Route to different credentials based on logic
|
||||
```
|
||||
Agent (Determine account) → Condition → Credential A or Credential B → Slack (Post)
|
||||
```
|
||||
|
||||
**Iterate over all Gmail accounts**
|
||||
```
|
||||
Credential (List, Provider: Gmail) → ForEach Loop → Gmail (Send) using <loop.currentItem.credentialId>
|
||||
```
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/credential-loop.png"
|
||||
alt="Credential List wired into a ForEach Loop"
|
||||
width={900}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## How to wire a Credential block
|
||||
|
||||
### Select Credential
|
||||
|
||||
1. Drop a **Credential** block and select your OAuth credential from the picker
|
||||
2. In the downstream block, switch to **advanced mode** on its credential field
|
||||
3. Enter `<credentialBlockName.credentialId>` as the value
|
||||
|
||||
<Tabs items={['Gmail', 'Slack']}>
|
||||
<Tab>
|
||||
In the Gmail block's credential field (advanced mode):
|
||||
```
|
||||
<myCredential.credentialId>
|
||||
```
|
||||
</Tab>
|
||||
<Tab>
|
||||
In the Slack block's credential field (advanced mode):
|
||||
```
|
||||
<myCredential.credentialId>
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### List Credentials
|
||||
|
||||
1. Drop a **Credential** block, set Operation to **List Credentials**
|
||||
2. Optionally select one or more **Providers** to narrow results (only your connected providers appear)
|
||||
3. Wire `<credentialBlockName.credentials>` into a **ForEach Loop** as the items source
|
||||
4. Inside the loop, reference `<loop.currentItem.credentialId>` in downstream blocks' credential fields
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Define once, reference many times**: When five blocks use the same Google account, use one Credential block and wire all five to `<credential.credentialId>` instead of selecting the account five times
|
||||
- **Outputs are safe to log**: The `credentialId` output is a UUID reference, not a secret. It is safe to inspect in execution logs
|
||||
- **Use for environment switching**: Pair with a Condition block to route to a production or staging OAuth credential based on a workflow variable
|
||||
- **Advanced mode is required**: Downstream blocks must be in advanced mode on their credential field to accept a dynamic reference
|
||||
- **Use List + ForEach for fan-out**: When you need to run the same action across all accounts of a provider, List Credentials feeds naturally into a ForEach loop
|
||||
- **Narrow by provider**: Use the Provider multiselect to filter to specific services — only providers you have credentials for are shown
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Does the Credential block expose my secret or token?", answer: "No. The block outputs a credential ID (a UUID), not the actual OAuth token. Downstream blocks receive the ID and resolve the token securely in their own execution context. Secrets never appear in workflow state, logs, or the canvas." },
|
||||
{ question: "What credential types does it support?", answer: "OAuth connected accounts only (Google, GitHub, Slack, etc.). Environment variables and service accounts cannot be resolved by ID in downstream blocks, so they are not supported." },
|
||||
{ question: "How is Select different from just copying a credential ID into advanced mode?", answer: "Functionally identical — both pass the same credential ID to the downstream block. The Credential block adds value when you need to use one credential in many blocks (change it once), or when you want to select between credentials dynamically using a Condition block." },
|
||||
{ question: "Can I list all OAuth credentials in my workspace?", answer: "Yes. Set the Operation to 'List Credentials'. Optionally filter by provider using the Provider multiselect. Wire the credentials output into a ForEach loop to process each credential individually." },
|
||||
{ question: "Can I use a Credential block output in a Function block?", answer: "Yes. Reference <credential.credentialId> in your Function block's code. Note that the function will receive the raw UUID string — if you need the resolved token, the downstream block must handle the resolution (as integration blocks do). The Function block does not automatically resolve credential IDs." },
|
||||
{ question: "What happens if the credential is deleted?", answer: "The Select operation will throw an error at execution time: 'Credential not found'. The List operation will simply omit the deleted credential from the results. Update the Credential block to select a valid credential before re-running." },
|
||||
]} />
|
||||
@@ -132,7 +132,7 @@ Detects personally identifiable information using Microsoft Presidio. Supports o
|
||||
**Use Cases:**
|
||||
- Block content containing sensitive personal information
|
||||
- Mask PII before logging or storing data
|
||||
- Compliance with GDPR, HIPAA, and other privacy regulations
|
||||
- Compliance with GDPR and other privacy regulations
|
||||
- Sanitize user inputs before processing
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"agent",
|
||||
"api",
|
||||
"condition",
|
||||
"credential",
|
||||
"evaluator",
|
||||
"function",
|
||||
"guardrails",
|
||||
|
||||
206
apps/docs/content/docs/en/credentials/google-service-account.mdx
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: Google Service Accounts
|
||||
description: Set up Google service accounts with domain-wide delegation for Gmail, Sheets, Drive, Calendar, and other Google services
|
||||
---
|
||||
|
||||
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'
|
||||
|
||||
Google service accounts with domain-wide delegation let your workflows access Google APIs on behalf of users in your Google Workspace domain — without requiring each user to complete an OAuth consent flow. This is ideal for automated workflows that need to send emails, read spreadsheets, or manage files across your organization.
|
||||
|
||||
For example, you could build a workflow that iterates through a list of employees, impersonates each one to read their Google Docs, and uploads the contents to a shared knowledge base — all without requiring any of those users to sign in.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before adding a service account to Sim, you need to configure it in the Google Cloud Console and Google Workspace Admin Console.
|
||||
|
||||
### 1. Create a Service Account in Google Cloud
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Go to the [Google Cloud Console](https://console.cloud.google.com/) and select your project (or create one)
|
||||
</Step>
|
||||
<Step>
|
||||
Navigate to **IAM & Admin** → **Service Accounts**
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Create Service Account**, give it a name and description, then click **Create and Continue**
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/gcp-create-service-account.png"
|
||||
alt="Google Cloud Console — Create service account form"
|
||||
width={700}
|
||||
height={500}
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
<Step>
|
||||
Skip the optional role and user access steps and click **Done**
|
||||
</Step>
|
||||
<Step>
|
||||
Click on the newly created service account, go to the **Keys** tab, and click **Add Key** → **Create new key**
|
||||
</Step>
|
||||
<Step>
|
||||
Select **JSON** as the key type and click **Create**. A JSON key file will download — keep this safe
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/gcp-create-private-key.png"
|
||||
alt="Google Cloud Console — Create private key dialog with JSON selected"
|
||||
width={700}
|
||||
height={400}
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="warn">
|
||||
The JSON key file contains your service account's private key. Treat it like a password — do not commit it to source control or share it publicly.
|
||||
</Callout>
|
||||
|
||||
### 2. Enable the Required APIs
|
||||
|
||||
In the Google Cloud Console, go to **APIs & Services** → **Library** and enable the APIs for the services your workflows will use. See the [scopes reference](#scopes-reference) below for the full list of APIs by service.
|
||||
|
||||
### 3. Set Up Domain-Wide Delegation
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
In the Google Cloud Console, go to **IAM & Admin** → **Service Accounts**, click on your service account, and copy the **Client ID** (the numeric ID, not the email)
|
||||
</Step>
|
||||
<Step>
|
||||
Open the [Google Workspace Admin Console](https://admin.google.com/) and navigate to **Security** → **Access and data control** → **API controls**
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Manage Domain Wide Delegation**, then click **Add new**
|
||||
</Step>
|
||||
<Step>
|
||||
Paste the **Client ID** from your service account, then add the OAuth scopes for the services your workflows need. Copy the full scope URLs from the [scopes reference](#scopes-reference) below — only authorize scopes for services you plan to use.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/gcp-add-client-id.png"
|
||||
alt="Google Workspace Admin Console — Add a new client ID with OAuth scopes"
|
||||
width={350}
|
||||
height={300}
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Authorize**
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
Domain-wide delegation must be configured by a Google Workspace admin. If you are not an admin, send the Client ID and required scopes to your admin.
|
||||
</Callout>
|
||||
|
||||
### Scopes Reference
|
||||
|
||||
The table below lists every Google service that supports service account authentication in Sim, the API to enable in Google Cloud Console, and the delegation scopes to authorize. Copy the scope string for each service you need and paste it into the Google Workspace Admin Console.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="whitespace-nowrap">Service</th>
|
||||
<th className="whitespace-nowrap">API to Enable</th>
|
||||
<th>Delegation Scopes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Gmail</td><td>Gmail API</td><td><code>{'https://www.googleapis.com/auth/gmail.send'}</code><br/><code>{'https://www.googleapis.com/auth/gmail.modify'}</code><br/><code>{'https://www.googleapis.com/auth/gmail.labels'}</code></td></tr>
|
||||
<tr><td>Google Sheets</td><td>Google Sheets API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
|
||||
<tr><td>Google Drive</td><td>Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
|
||||
<tr><td>Google Docs</td><td>Google Docs API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
|
||||
<tr><td>Google Slides</td><td>Google Slides API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/drive.file'}</code></td></tr>
|
||||
<tr><td>Google Forms</td><td>Google Forms API, Google Drive API</td><td><code>{'https://www.googleapis.com/auth/drive'}</code><br/><code>{'https://www.googleapis.com/auth/forms.body'}</code><br/><code>{'https://www.googleapis.com/auth/forms.responses.readonly'}</code></td></tr>
|
||||
<tr><td>Google Calendar</td><td>Google Calendar API</td><td><code>{'https://www.googleapis.com/auth/calendar'}</code></td></tr>
|
||||
<tr><td>Google Contacts</td><td>People API</td><td><code>{'https://www.googleapis.com/auth/contacts'}</code></td></tr>
|
||||
<tr><td>BigQuery</td><td>BigQuery API</td><td><code>{'https://www.googleapis.com/auth/bigquery'}</code></td></tr>
|
||||
<tr><td>Google Tasks</td><td>Tasks API</td><td><code>{'https://www.googleapis.com/auth/tasks'}</code></td></tr>
|
||||
<tr><td>Google Vault</td><td>Vault API, Cloud Storage API</td><td><code>{'https://www.googleapis.com/auth/ediscovery'}</code><br/><code>{'https://www.googleapis.com/auth/devstorage.read_only'}</code></td></tr>
|
||||
<tr><td>Google Groups</td><td>Admin SDK API</td><td><code>{'https://www.googleapis.com/auth/admin.directory.group'}</code><br/><code>{'https://www.googleapis.com/auth/admin.directory.group.member'}</code></td></tr>
|
||||
<tr><td>Google Meet</td><td>Google Meet API</td><td><code>{'https://www.googleapis.com/auth/meetings.space.created'}</code><br/><code>{'https://www.googleapis.com/auth/meetings.space.readonly'}</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Callout type="info">
|
||||
You only need to enable APIs and authorize scopes for the services you plan to use. When authorizing multiple services, combine their scope strings with commas into a single entry in the Admin Console.
|
||||
</Callout>
|
||||
|
||||
## Adding the Service Account to Sim
|
||||
|
||||
Once Google Cloud and Workspace are configured, add the service account as a credential in Sim.
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
Open your workspace **Settings** and go to the **Integrations** tab
|
||||
</Step>
|
||||
<Step>
|
||||
Search for "Google Service Account" and click **Connect**
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/integrations-service-account.png"
|
||||
alt="Integrations page showing Google Service Account"
|
||||
width={800}
|
||||
height={150}
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
<Step>
|
||||
Paste the full contents of your JSON key file into the text area
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/add-service-account.png"
|
||||
alt="Add Google Service Account dialog"
|
||||
width={350}
|
||||
height={420}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
</Step>
|
||||
<Step>
|
||||
Give the credential a display name (the service account email is used by default)
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Save**
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
The JSON key file is validated for the required fields (`type`, `client_email`, `private_key`, `project_id`) and encrypted before being stored.
|
||||
|
||||
## Using Delegated Access in Workflows
|
||||
|
||||
When you use a Google block (Gmail, Sheets, Drive, etc.) in a workflow and select a service account credential, an **Impersonate User Email** field appears below the credential selector.
|
||||
|
||||
Enter the email address of the Google Workspace user you want the service account to act as. For example, if you enter `alice@yourcompany.com`, the workflow will send emails from Alice's account, read her spreadsheets, or access her calendar — depending on the scopes you authorized.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/credentials/workflow-impersonated-account.png"
|
||||
alt="Gmail block in a workflow showing the Impersonated Account field with a service account credential"
|
||||
width={800}
|
||||
height={350}
|
||||
className="my-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Callout type="warn">
|
||||
The impersonated email must belong to a user in the Google Workspace domain where you configured domain-wide delegation. Impersonating external email addresses will fail.
|
||||
</Callout>
|
||||
|
||||
<FAQ items={[
|
||||
{ question: "Can I use a service account without domain-wide delegation?", answer: "Yes, but it will only be able to access resources owned by the service account itself (e.g., spreadsheets shared directly with the service account email). Without delegation, you cannot impersonate users or access their personal data like Gmail." },
|
||||
{ question: "What happens if the impersonation email field is left blank?", answer: "The service account will authenticate as itself. This works for accessing shared resources (like a Google Sheet shared with the service account email) but will fail for user-specific APIs like Gmail." },
|
||||
{ question: "Can I use the same service account for multiple Google services?", answer: "Yes. A single service account can be used across Gmail, Sheets, Drive, Calendar, and other Google services — as long as the required API is enabled in Google Cloud and the corresponding scopes are authorized in the Workspace admin console." },
|
||||
{ question: "How do I rotate the service account key?", answer: "Create a new JSON key in the Google Cloud Console under your service account's Keys tab, then update the credential in Sim with the new key. Delete the old key from Google Cloud once the new one is working." },
|
||||
{ question: "Does the impersonated user need a Google Workspace license?", answer: "Yes. Domain-wide delegation only works with users who have a Google Workspace account in the domain. Consumer Gmail accounts (e.g., @gmail.com) cannot be impersonated." },
|
||||
]} />
|
||||
5
apps/docs/content/docs/en/credentials/meta.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Credentials",
|
||||
"pages": ["index", "google-service-account"],
|
||||
"defaultOpen": false
|
||||
}
|
||||
@@ -195,6 +195,17 @@ By default, your usage is capped at the credits included in your plan. To allow
|
||||
|
||||
Max (individual) shares the same rate limits as team plans. Team plans (Pro or Max for Teams) use the Max-tier rate limits.
|
||||
|
||||
### Concurrent Execution Limits
|
||||
|
||||
| Plan | Concurrent Executions |
|
||||
|------|----------------------|
|
||||
| **Free** | 5 |
|
||||
| **Pro** | 50 |
|
||||
| **Max / Team** | 200 |
|
||||
| **Enterprise** | 200 (customizable) |
|
||||
|
||||
Concurrent execution limits control how many workflow executions can run simultaneously within a workspace. When the limit is reached, new executions are queued and admitted as running executions complete. Manual runs from the editor are not subject to these limits.
|
||||
|
||||
### File Storage
|
||||
|
||||
| Plan | Storage |
|
||||
|
||||
592
apps/docs/content/docs/en/tools/agentmail.mdx
Normal file
@@ -0,0 +1,592 @@
|
||||
---
|
||||
title: AgentMail
|
||||
description: Manage email inboxes, threads, and messages with AgentMail
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="agentmail"
|
||||
color="#000000"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[AgentMail](https://agentmail.to/) is an API-first email platform built for agents and automation. AgentMail lets you create email inboxes on the fly, send and receive messages, reply to threads, manage drafts, and organize conversations with labels — all through a simple REST API designed for programmatic access.
|
||||
|
||||
**Why AgentMail?**
|
||||
- **Agent-Native Email:** Purpose-built for AI agents and automation — create inboxes, send messages, and manage threads without human-facing UI overhead.
|
||||
- **Full Email Lifecycle:** Send new messages, reply to threads, forward emails, manage drafts, and schedule sends — all from a single API.
|
||||
- **Thread & Conversation Management:** Organize emails into threads with full read, reply, forward, and label support for structured conversation tracking.
|
||||
- **Draft Workflow:** Compose drafts, update them, schedule sends, and dispatch when ready — perfect for review-before-send workflows.
|
||||
- **Label Organization:** Tag threads and messages with custom labels for filtering, routing, and downstream automation.
|
||||
|
||||
**Using AgentMail in Sim**
|
||||
|
||||
Sim's AgentMail integration connects your agentic workflows directly to AgentMail using an API key. With 20 operations spanning inboxes, threads, messages, and drafts, you can build powerful email automations without writing backend code.
|
||||
|
||||
**Key benefits of using AgentMail in Sim:**
|
||||
- **Dynamic inbox creation:** Spin up new inboxes on the fly for each agent, workflow, or customer — perfect for multi-tenant email handling.
|
||||
- **Automated email processing:** List and read incoming messages, then trigger downstream actions based on content, sender, or labels.
|
||||
- **Conversational email:** Reply to threads and forward messages to keep conversations flowing naturally within your automated workflows.
|
||||
- **Draft and review workflows:** Create drafts, update them with AI-generated content, and send when approved — ideal for human-in-the-loop patterns.
|
||||
- **Email organization:** Apply labels to threads and messages to categorize, filter, and route emails through your automation pipeline.
|
||||
|
||||
Whether you're building an AI email assistant, automating customer support replies, processing incoming leads, or managing multi-agent email workflows, AgentMail in Sim gives you direct, secure access to the full AgentMail API — no middleware required. Simply configure your API key, select the operation you need, and let Sim handle the rest.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate AgentMail into your workflow. Create and manage email inboxes, send and receive messages, reply to threads, manage drafts, and organize threads with labels. Requires API Key.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `agentmail_create_draft`
|
||||
|
||||
Create a new email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to create the draft in |
|
||||
| `to` | string | No | Recipient email addresses \(comma-separated\) |
|
||||
| `subject` | string | No | Draft subject line |
|
||||
| `text` | string | No | Plain text draft body |
|
||||
| `html` | string | No | HTML draft body |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
| `inReplyTo` | string | No | ID of message being replied to |
|
||||
| `sendAt` | string | No | ISO 8601 timestamp to schedule sending |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `draftId` | string | Unique identifier for the draft |
|
||||
| `inboxId` | string | Inbox the draft belongs to |
|
||||
| `subject` | string | Draft subject |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `preview` | string | Draft preview text |
|
||||
| `labels` | array | Labels assigned to the draft |
|
||||
| `inReplyTo` | string | Message ID this draft replies to |
|
||||
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| `sendAt` | string | Scheduled send time |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_create_inbox`
|
||||
|
||||
Create a new email inbox with AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `username` | string | No | Username for the inbox email address |
|
||||
| `domain` | string | No | Domain for the inbox email address |
|
||||
| `displayName` | string | No | Display name for the inbox |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxId` | string | Unique identifier for the inbox |
|
||||
| `email` | string | Email address of the inbox |
|
||||
| `displayName` | string | Display name of the inbox |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_delete_draft`
|
||||
|
||||
Delete an email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the draft |
|
||||
| `draftId` | string | Yes | ID of the draft to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the draft was successfully deleted |
|
||||
|
||||
### `agentmail_delete_inbox`
|
||||
|
||||
Delete an email inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the inbox was successfully deleted |
|
||||
|
||||
### `agentmail_delete_thread`
|
||||
|
||||
Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the thread |
|
||||
| `threadId` | string | Yes | ID of the thread to delete |
|
||||
| `permanent` | boolean | No | Force permanent deletion instead of moving to trash |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the thread was successfully deleted |
|
||||
|
||||
### `agentmail_forward_message`
|
||||
|
||||
Forward an email message to new recipients in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the message |
|
||||
| `messageId` | string | Yes | ID of the message to forward |
|
||||
| `to` | string | Yes | Recipient email addresses \(comma-separated\) |
|
||||
| `subject` | string | No | Override subject line |
|
||||
| `text` | string | No | Additional plain text to prepend |
|
||||
| `html` | string | No | Additional HTML to prepend |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | ID of the forwarded message |
|
||||
| `threadId` | string | ID of the thread |
|
||||
|
||||
### `agentmail_get_draft`
|
||||
|
||||
Get details of a specific email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox the draft belongs to |
|
||||
| `draftId` | string | Yes | ID of the draft to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `draftId` | string | Unique identifier for the draft |
|
||||
| `inboxId` | string | Inbox the draft belongs to |
|
||||
| `subject` | string | Draft subject |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `preview` | string | Draft preview text |
|
||||
| `labels` | array | Labels assigned to the draft |
|
||||
| `inReplyTo` | string | Message ID this draft replies to |
|
||||
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| `sendAt` | string | Scheduled send time |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_get_inbox`
|
||||
|
||||
Get details of a specific email inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxId` | string | Unique identifier for the inbox |
|
||||
| `email` | string | Email address of the inbox |
|
||||
| `displayName` | string | Display name of the inbox |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_get_message`
|
||||
|
||||
Get details of a specific email message in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the message |
|
||||
| `messageId` | string | Yes | ID of the message to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | Unique identifier for the message |
|
||||
| `threadId` | string | ID of the thread this message belongs to |
|
||||
| `from` | string | Sender email address |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `subject` | string | Message subject |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
|
||||
### `agentmail_get_thread`
|
||||
|
||||
Get details of a specific email thread including messages in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the thread |
|
||||
| `threadId` | string | Yes | ID of the thread to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threadId` | string | Unique identifier for the thread |
|
||||
| `subject` | string | Thread subject |
|
||||
| `senders` | array | List of sender email addresses |
|
||||
| `recipients` | array | List of recipient email addresses |
|
||||
| `messageCount` | number | Number of messages in the thread |
|
||||
| `labels` | array | Labels assigned to the thread |
|
||||
| `lastMessageAt` | string | Timestamp of last message |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
| `messages` | array | Messages in the thread |
|
||||
| ↳ `messageId` | string | Unique identifier for the message |
|
||||
| ↳ `from` | string | Sender email address |
|
||||
| ↳ `to` | array | Recipient email addresses |
|
||||
| ↳ `cc` | array | CC email addresses |
|
||||
| ↳ `bcc` | array | BCC email addresses |
|
||||
| ↳ `subject` | string | Message subject |
|
||||
| ↳ `text` | string | Plain text content |
|
||||
| ↳ `html` | string | HTML content |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
|
||||
### `agentmail_list_drafts`
|
||||
|
||||
List email drafts in an inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to list drafts from |
|
||||
| `limit` | number | No | Maximum number of drafts to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `drafts` | array | List of drafts |
|
||||
| ↳ `draftId` | string | Unique identifier for the draft |
|
||||
| ↳ `inboxId` | string | Inbox the draft belongs to |
|
||||
| ↳ `subject` | string | Draft subject |
|
||||
| ↳ `to` | array | Recipient email addresses |
|
||||
| ↳ `cc` | array | CC email addresses |
|
||||
| ↳ `bcc` | array | BCC email addresses |
|
||||
| ↳ `preview` | string | Draft preview text |
|
||||
| ↳ `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| ↳ `sendAt` | string | Scheduled send time |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp |
|
||||
| `count` | number | Total number of drafts |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_list_inboxes`
|
||||
|
||||
List all email inboxes in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `limit` | number | No | Maximum number of inboxes to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxes` | array | List of inboxes |
|
||||
| ↳ `inboxId` | string | Unique identifier for the inbox |
|
||||
| ↳ `email` | string | Email address of the inbox |
|
||||
| ↳ `displayName` | string | Display name of the inbox |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp |
|
||||
| `count` | number | Total number of inboxes |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_list_messages`
|
||||
|
||||
List messages in an inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to list messages from |
|
||||
| `limit` | number | No | Maximum number of messages to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messages` | array | List of messages in the inbox |
|
||||
| ↳ `messageId` | string | Unique identifier for the message |
|
||||
| ↳ `from` | string | Sender email address |
|
||||
| ↳ `to` | array | Recipient email addresses |
|
||||
| ↳ `subject` | string | Message subject |
|
||||
| ↳ `preview` | string | Message preview text |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| `count` | number | Total number of messages |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_list_threads`
|
||||
|
||||
List email threads in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to list threads from |
|
||||
| `limit` | number | No | Maximum number of threads to return |
|
||||
| `pageToken` | string | No | Pagination token for next page of results |
|
||||
| `labels` | string | No | Comma-separated labels to filter threads by |
|
||||
| `before` | string | No | Filter threads before this ISO 8601 timestamp |
|
||||
| `after` | string | No | Filter threads after this ISO 8601 timestamp |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threads` | array | List of email threads |
|
||||
| ↳ `threadId` | string | Unique identifier for the thread |
|
||||
| ↳ `subject` | string | Thread subject |
|
||||
| ↳ `senders` | array | List of sender email addresses |
|
||||
| ↳ `recipients` | array | List of recipient email addresses |
|
||||
| ↳ `messageCount` | number | Number of messages in the thread |
|
||||
| ↳ `lastMessageAt` | string | Timestamp of last message |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp |
|
||||
| `count` | number | Total number of threads |
|
||||
| `nextPageToken` | string | Token for retrieving the next page |
|
||||
|
||||
### `agentmail_reply_message`
|
||||
|
||||
Reply to an existing email message in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to reply from |
|
||||
| `messageId` | string | Yes | ID of the message to reply to |
|
||||
| `text` | string | No | Plain text reply body |
|
||||
| `html` | string | No | HTML reply body |
|
||||
| `to` | string | No | Override recipient email addresses \(comma-separated\) |
|
||||
| `cc` | string | No | CC email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC email addresses \(comma-separated\) |
|
||||
| `replyAll` | boolean | No | Reply to all recipients of the original message |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | ID of the sent reply message |
|
||||
| `threadId` | string | ID of the thread |
|
||||
|
||||
### `agentmail_send_draft`
|
||||
|
||||
Send an existing email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the draft |
|
||||
| `draftId` | string | Yes | ID of the draft to send |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | ID of the sent message |
|
||||
| `threadId` | string | ID of the thread |
|
||||
|
||||
### `agentmail_send_message`
|
||||
|
||||
Send an email message from an AgentMail inbox
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to send from |
|
||||
| `to` | string | Yes | Recipient email address \(comma-separated for multiple\) |
|
||||
| `subject` | string | Yes | Email subject line |
|
||||
| `text` | string | No | Plain text email body |
|
||||
| `html` | string | No | HTML email body |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threadId` | string | ID of the created thread |
|
||||
| `messageId` | string | ID of the sent message |
|
||||
| `subject` | string | Email subject line |
|
||||
| `to` | string | Recipient email address |
|
||||
|
||||
### `agentmail_update_draft`
|
||||
|
||||
Update an existing email draft in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the draft |
|
||||
| `draftId` | string | Yes | ID of the draft to update |
|
||||
| `to` | string | No | Recipient email addresses \(comma-separated\) |
|
||||
| `subject` | string | No | Draft subject line |
|
||||
| `text` | string | No | Plain text draft body |
|
||||
| `html` | string | No | HTML draft body |
|
||||
| `cc` | string | No | CC recipient email addresses \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipient email addresses \(comma-separated\) |
|
||||
| `sendAt` | string | No | ISO 8601 timestamp to schedule sending |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `draftId` | string | Unique identifier for the draft |
|
||||
| `inboxId` | string | Inbox the draft belongs to |
|
||||
| `subject` | string | Draft subject |
|
||||
| `to` | array | Recipient email addresses |
|
||||
| `cc` | array | CC email addresses |
|
||||
| `bcc` | array | BCC email addresses |
|
||||
| `text` | string | Plain text content |
|
||||
| `html` | string | HTML content |
|
||||
| `preview` | string | Draft preview text |
|
||||
| `labels` | array | Labels assigned to the draft |
|
||||
| `inReplyTo` | string | Message ID this draft replies to |
|
||||
| `sendStatus` | string | Send status \(scheduled, sending, failed\) |
|
||||
| `sendAt` | string | Scheduled send time |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_update_inbox`
|
||||
|
||||
Update the display name of an email inbox in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox to update |
|
||||
| `displayName` | string | Yes | New display name for the inbox |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inboxId` | string | Unique identifier for the inbox |
|
||||
| `email` | string | Email address of the inbox |
|
||||
| `displayName` | string | Display name of the inbox |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last updated timestamp |
|
||||
|
||||
### `agentmail_update_message`
|
||||
|
||||
Add or remove labels on an email message in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the message |
|
||||
| `messageId` | string | Yes | ID of the message to update |
|
||||
| `addLabels` | string | No | Comma-separated labels to add to the message |
|
||||
| `removeLabels` | string | No | Comma-separated labels to remove from the message |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | Unique identifier for the message |
|
||||
| `labels` | array | Current labels on the message |
|
||||
|
||||
### `agentmail_update_thread`
|
||||
|
||||
Add or remove labels on an email thread in AgentMail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | AgentMail API key |
|
||||
| `inboxId` | string | Yes | ID of the inbox containing the thread |
|
||||
| `threadId` | string | Yes | ID of the thread to update |
|
||||
| `addLabels` | string | No | Comma-separated labels to add to the thread |
|
||||
| `removeLabels` | string | No | Comma-separated labels to remove from the thread |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `threadId` | string | Unique identifier for the thread |
|
||||
| `labels` | array | Current labels on the thread |
|
||||
|
||||
|
||||
@@ -359,6 +359,35 @@ List tasks in Attio, optionally filtered by record, assignee, or completion stat
|
||||
| ↳ `createdAt` | string | When the task was created |
|
||||
| `count` | number | Number of tasks returned |
|
||||
|
||||
### `attio_get_task`
|
||||
|
||||
Get a single task by ID from Attio
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `taskId` | string | Yes | The ID of the task to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `taskId` | string | The task ID |
|
||||
| `content` | string | The task content |
|
||||
| `deadlineAt` | string | The task deadline |
|
||||
| `isCompleted` | boolean | Whether the task is completed |
|
||||
| `linkedRecords` | array | Records linked to this task |
|
||||
| ↳ `targetObjectId` | string | The linked object ID |
|
||||
| ↳ `targetRecordId` | string | The linked record ID |
|
||||
| `assignees` | array | Task assignees |
|
||||
| ↳ `type` | string | The assignee actor type \(e.g. workspace-member\) |
|
||||
| ↳ `id` | string | The assignee actor ID |
|
||||
| `createdByActor` | object | The actor who created this task |
|
||||
| ↳ `type` | string | The actor type \(e.g. workspace-member, api-token, system\) |
|
||||
| ↳ `id` | string | The actor ID |
|
||||
| `createdAt` | string | When the task was created |
|
||||
|
||||
### `attio_create_task`
|
||||
|
||||
Create a task in Attio
|
||||
@@ -1012,8 +1041,8 @@ Update a webhook in Attio (target URL and/or subscriptions)
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `webhookId` | string | Yes | The webhook ID to update |
|
||||
| `targetUrl` | string | Yes | HTTPS target URL for webhook delivery |
|
||||
| `subscriptions` | string | Yes | JSON array of subscriptions, e.g. \[\{"event_type":"note.created"\}\] |
|
||||
| `targetUrl` | string | No | HTTPS target URL for webhook delivery |
|
||||
| `subscriptions` | string | No | JSON array of subscriptions, e.g. \[\{"event_type":"note.created"\}\] |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
39
apps/docs/content/docs/en/tools/extend.mdx
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Extend
|
||||
description: Parse and extract content from documents
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="extend_v2"
|
||||
color="#000000"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Extend AI into the workflow. Parse and extract structured content from documents or file references.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `extend_parser`
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `filePath` | string | No | URL to a document to be processed |
|
||||
| `file` | file | No | Document file to be processed |
|
||||
| `fileUpload` | object | No | File upload data from file-upload component |
|
||||
| `outputFormat` | string | No | Target output format \(markdown or spatial\). Defaults to markdown. |
|
||||
| `chunking` | string | No | Chunking strategy \(page, document, or section\). Defaults to page. |
|
||||
| `engine` | string | No | Parsing engine \(parse_performance or parse_light\). Defaults to parse_performance. |
|
||||
| `apiKey` | string | Yes | Extend API key |
|
||||
|
||||
#### Output
|
||||
|
||||
This tool does not produce any outputs.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: File
|
||||
description: Read and parse multiple files
|
||||
description: Read and write workspace files
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -27,7 +27,7 @@ The File Parser tool is particularly useful for scenarios where your agents need
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Upload files directly or import from external URLs to get UserFile objects for use in other blocks.
|
||||
Read and parse files from uploads or URLs, write new workspace files, or append content to existing files.
|
||||
|
||||
|
||||
|
||||
@@ -52,4 +52,45 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
|
||||
| `files` | file[] | Parsed files as UserFile objects |
|
||||
| `combinedContent` | string | Combined content of all parsed files |
|
||||
|
||||
### `file_write`
|
||||
|
||||
Create a new workspace file. If a file with the same name already exists, a numeric suffix is added (e.g.,
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileName` | string | Yes | File name \(e.g., "data.csv"\). If a file with this name exists, a numeric suffix is added automatically. |
|
||||
| `content` | string | Yes | The text content to write to the file. |
|
||||
| `contentType` | string | No | MIME type for new files \(e.g., "text/plain"\). Auto-detected from file extension if omitted. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `size` | number | File size in bytes |
|
||||
| `url` | string | URL to access the file |
|
||||
|
||||
### `file_append`
|
||||
|
||||
Append content to an existing workspace file. The file must already exist. Content is added to the end of the file.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileName` | string | Yes | Name of an existing workspace file to append to. |
|
||||
| `content` | string | Yes | The text content to append to the file. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | File ID |
|
||||
| `name` | string | File name |
|
||||
| `size` | number | File size in bytes |
|
||||
| `url` | string | URL to access the file |
|
||||
|
||||
|
||||
|
||||
92
apps/docs/content/docs/en/tools/granola.mdx
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: Granola
|
||||
description: Access meeting notes and transcripts from Granola
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="granola"
|
||||
color="#B2C147"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Granola into your workflow to retrieve meeting notes, summaries, attendees, and transcripts.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `granola_list_notes`
|
||||
|
||||
Lists meeting notes from Granola with optional date filters and pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Granola API key |
|
||||
| `createdBefore` | string | No | Return notes created before this date \(ISO 8601\) |
|
||||
| `createdAfter` | string | No | Return notes created after this date \(ISO 8601\) |
|
||||
| `updatedAfter` | string | No | Return notes updated after this date \(ISO 8601\) |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
| `pageSize` | number | No | Number of notes per page \(1-30, default 10\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `notes` | json | List of meeting notes |
|
||||
| ↳ `id` | string | Note ID |
|
||||
| ↳ `title` | string | Note title |
|
||||
| ↳ `ownerName` | string | Note owner name |
|
||||
| ↳ `ownerEmail` | string | Note owner email |
|
||||
| ↳ `createdAt` | string | Creation timestamp |
|
||||
| ↳ `updatedAt` | string | Last update timestamp |
|
||||
| `hasMore` | boolean | Whether more notes are available |
|
||||
| `cursor` | string | Pagination cursor for the next page |
|
||||
|
||||
### `granola_get_note`
|
||||
|
||||
Retrieves a specific meeting note from Granola by ID, including summary, attendees, calendar event details, and optionally the transcript.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Granola API key |
|
||||
| `noteId` | string | Yes | The note ID \(e.g., not_1d3tmYTlCICgjy\) |
|
||||
| `includeTranscript` | string | No | Whether to include the meeting transcript |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Note ID |
|
||||
| `title` | string | Note title |
|
||||
| `ownerName` | string | Note owner name |
|
||||
| `ownerEmail` | string | Note owner email |
|
||||
| `createdAt` | string | Creation timestamp |
|
||||
| `updatedAt` | string | Last update timestamp |
|
||||
| `summaryText` | string | Plain text summary of the meeting |
|
||||
| `summaryMarkdown` | string | Markdown-formatted summary of the meeting |
|
||||
| `attendees` | json | Meeting attendees |
|
||||
| ↳ `name` | string | Attendee name |
|
||||
| ↳ `email` | string | Attendee email |
|
||||
| `folders` | json | Folders the note belongs to |
|
||||
| ↳ `id` | string | Folder ID |
|
||||
| ↳ `name` | string | Folder name |
|
||||
| `calendarEventTitle` | string | Calendar event title |
|
||||
| `calendarOrganiser` | string | Calendar event organiser email |
|
||||
| `calendarEventId` | string | Calendar event ID |
|
||||
| `scheduledStartTime` | string | Scheduled start time |
|
||||
| `scheduledEndTime` | string | Scheduled end time |
|
||||
| `invitees` | json | Calendar event invitee emails |
|
||||
| `transcript` | json | Meeting transcript entries \(only if requested\) |
|
||||
| ↳ `speaker` | string | Speaker source \(microphone or speaker\) |
|
||||
| ↳ `text` | string | Transcript text |
|
||||
| ↳ `startTime` | string | Segment start time |
|
||||
| ↳ `endTime` | string | Segment end time |
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ Retrieve all users from HubSpot account
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | string | No | Number of results to return \(default: 100, max: 100\) |
|
||||
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -53,6 +54,9 @@ Retrieve all users from HubSpot account
|
||||
| ↳ `primaryTeamId` | string | Primary team ID |
|
||||
| ↳ `secondaryTeamIds` | array | Secondary team IDs |
|
||||
| ↳ `superAdmin` | boolean | Whether user is a super admin |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `totalItems` | number | Total number of users returned |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
@@ -230,7 +234,7 @@ Search for contacts in HubSpot using filters, sorting, and queries
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "CONTAINS"\), and "value" |
|
||||
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "CONTAINS_TOKEN", "GT"\), and "value" |
|
||||
| `sorts` | array | No | Array of sort objects as JSON with "propertyName" and "direction" \("ASCENDING" or "DESCENDING"\) |
|
||||
| `query` | string | No | Search query string to match against contact name, email, and other text fields |
|
||||
| `properties` | array | No | Array of HubSpot property names to return \(e.g., \["email", "firstname", "lastname", "phone"\]\) |
|
||||
@@ -449,7 +453,7 @@ Search for companies in HubSpot using filters, sorting, and queries
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "CONTAINS"\), and "value" |
|
||||
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "CONTAINS_TOKEN", "GT"\), and "value" |
|
||||
| `sorts` | array | No | Array of sort objects as JSON with "propertyName" and "direction" \("ASCENDING" or "DESCENDING"\) |
|
||||
| `query` | string | No | Search query string to match against company name, domain, and other text fields |
|
||||
| `properties` | array | No | Array of HubSpot property names to return \(e.g., \["name", "domain", "industry"\]\) |
|
||||
@@ -499,7 +503,7 @@ Retrieve all deals from HubSpot account with pagination support
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | string | No | Maximum number of results per page \(max 100, default 100\) |
|
||||
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
|
||||
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "dealname,amount,dealstage"\) |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
|
||||
@@ -529,4 +533,887 @@ Retrieve all deals from HubSpot account with pagination support
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_get_deal`
|
||||
|
||||
Retrieve a single deal by ID from HubSpot
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `dealId` | string | Yes | The HubSpot deal ID to retrieve |
|
||||
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "dealname,amount,dealstage"\) |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deal` | object | HubSpot deal record |
|
||||
| ↳ `dealname` | string | Deal name |
|
||||
| ↳ `amount` | string | Deal amount |
|
||||
| ↳ `dealstage` | string | Current deal stage |
|
||||
| ↳ `pipeline` | string | Pipeline the deal is in |
|
||||
| ↳ `closedate` | string | Expected close date \(ISO 8601\) |
|
||||
| ↳ `dealtype` | string | Deal type \(New Business, Existing Business, etc.\) |
|
||||
| ↳ `description` | string | Deal description |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Deal creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| ↳ `num_associated_contacts` | string | Number of associated contacts |
|
||||
| `dealId` | string | The retrieved deal ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_create_deal`
|
||||
|
||||
Create a new deal in HubSpot. Requires at least a dealname property
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `properties` | object | Yes | Deal properties as JSON object. Must include dealname \(e.g., \{"dealname": "New Deal", "amount": "5000", "dealstage": "appointmentscheduled"\}\) |
|
||||
| `associations` | array | No | Array of associations to create with the deal as JSON. Each object should have "to.id" and "types" array with "associationCategory" and "associationTypeId" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deal` | object | HubSpot deal record |
|
||||
| ↳ `dealname` | string | Deal name |
|
||||
| ↳ `amount` | string | Deal amount |
|
||||
| ↳ `dealstage` | string | Current deal stage |
|
||||
| ↳ `pipeline` | string | Pipeline the deal is in |
|
||||
| ↳ `closedate` | string | Expected close date \(ISO 8601\) |
|
||||
| ↳ `dealtype` | string | Deal type \(New Business, Existing Business, etc.\) |
|
||||
| ↳ `description` | string | Deal description |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Deal creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| ↳ `num_associated_contacts` | string | Number of associated contacts |
|
||||
| `dealId` | string | The created deal ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_update_deal`
|
||||
|
||||
Update an existing deal in HubSpot by ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `dealId` | string | Yes | The HubSpot deal ID to update |
|
||||
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
|
||||
| `properties` | object | Yes | Deal properties to update as JSON object \(e.g., \{"amount": "10000", "dealstage": "closedwon"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deal` | object | HubSpot deal record |
|
||||
| ↳ `dealname` | string | Deal name |
|
||||
| ↳ `amount` | string | Deal amount |
|
||||
| ↳ `dealstage` | string | Current deal stage |
|
||||
| ↳ `pipeline` | string | Pipeline the deal is in |
|
||||
| ↳ `closedate` | string | Expected close date \(ISO 8601\) |
|
||||
| ↳ `dealtype` | string | Deal type \(New Business, Existing Business, etc.\) |
|
||||
| ↳ `description` | string | Deal description |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Deal creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| ↳ `num_associated_contacts` | string | Number of associated contacts |
|
||||
| `dealId` | string | The updated deal ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_search_deals`
|
||||
|
||||
Search for deals in HubSpot using filters, sorting, and queries
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "NEQ", "CONTAINS_TOKEN", "NOT_CONTAINS_TOKEN"\), and "value" |
|
||||
| `sorts` | array | No | Array of sort objects as JSON with "propertyName" and "direction" \("ASCENDING" or "DESCENDING"\) |
|
||||
| `query` | string | No | Search query string to match against deal name and other text fields |
|
||||
| `properties` | array | No | Array of HubSpot property names to return \(e.g., \["dealname", "amount", "dealstage"\]\) |
|
||||
| `limit` | number | No | Maximum number of results to return \(max 200\) |
|
||||
| `after` | string | No | Pagination cursor for next page \(from previous response\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deals` | array | Array of HubSpot deal records |
|
||||
| ↳ `dealname` | string | Deal name |
|
||||
| ↳ `amount` | string | Deal amount |
|
||||
| ↳ `dealstage` | string | Current deal stage |
|
||||
| ↳ `pipeline` | string | Pipeline the deal is in |
|
||||
| ↳ `closedate` | string | Expected close date \(ISO 8601\) |
|
||||
| ↳ `dealtype` | string | Deal type \(New Business, Existing Business, etc.\) |
|
||||
| ↳ `description` | string | Deal description |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Deal creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| ↳ `num_associated_contacts` | string | Number of associated contacts |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| `total` | number | Total number of matching deals |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_list_tickets`
|
||||
|
||||
Retrieve all tickets from HubSpot account with pagination support
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
|
||||
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "subject,content,hs_ticket_priority"\) |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tickets` | array | Array of HubSpot ticket records |
|
||||
| ↳ `subject` | string | Ticket subject/name |
|
||||
| ↳ `content` | string | Ticket content/description |
|
||||
| ↳ `hs_pipeline` | string | Pipeline the ticket is in |
|
||||
| ↳ `hs_pipeline_stage` | string | Current pipeline stage |
|
||||
| ↳ `hs_ticket_priority` | string | Ticket priority \(LOW, MEDIUM, HIGH\) |
|
||||
| ↳ `hs_ticket_category` | string | Ticket category |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Ticket creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_get_ticket`
|
||||
|
||||
Retrieve a single ticket by ID from HubSpot
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `ticketId` | string | Yes | The HubSpot ticket ID to retrieve |
|
||||
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "subject,content,hs_ticket_priority"\) |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ticket` | object | HubSpot ticket record |
|
||||
| ↳ `subject` | string | Ticket subject/name |
|
||||
| ↳ `content` | string | Ticket content/description |
|
||||
| ↳ `hs_pipeline` | string | Pipeline the ticket is in |
|
||||
| ↳ `hs_pipeline_stage` | string | Current pipeline stage |
|
||||
| ↳ `hs_ticket_priority` | string | Ticket priority \(LOW, MEDIUM, HIGH\) |
|
||||
| ↳ `hs_ticket_category` | string | Ticket category |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Ticket creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `ticketId` | string | The retrieved ticket ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_create_ticket`
|
||||
|
||||
Create a new ticket in HubSpot. Requires subject and hs_pipeline_stage properties
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `properties` | object | Yes | Ticket properties as JSON object. Must include subject and hs_pipeline_stage \(e.g., \{"subject": "Support request", "hs_pipeline_stage": "1", "hs_ticket_priority": "HIGH"\}\) |
|
||||
| `associations` | array | No | Array of associations to create with the ticket as JSON. Each object should have "to.id" and "types" array with "associationCategory" and "associationTypeId" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ticket` | object | HubSpot ticket record |
|
||||
| ↳ `subject` | string | Ticket subject/name |
|
||||
| ↳ `content` | string | Ticket content/description |
|
||||
| ↳ `hs_pipeline` | string | Pipeline the ticket is in |
|
||||
| ↳ `hs_pipeline_stage` | string | Current pipeline stage |
|
||||
| ↳ `hs_ticket_priority` | string | Ticket priority \(LOW, MEDIUM, HIGH\) |
|
||||
| ↳ `hs_ticket_category` | string | Ticket category |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Ticket creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `ticketId` | string | The created ticket ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_update_ticket`
|
||||
|
||||
Update an existing ticket in HubSpot by ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `ticketId` | string | Yes | The HubSpot ticket ID to update |
|
||||
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
|
||||
| `properties` | object | Yes | Ticket properties to update as JSON object \(e.g., \{"subject": "Updated subject", "hs_ticket_priority": "HIGH"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ticket` | object | HubSpot ticket record |
|
||||
| ↳ `subject` | string | Ticket subject/name |
|
||||
| ↳ `content` | string | Ticket content/description |
|
||||
| ↳ `hs_pipeline` | string | Pipeline the ticket is in |
|
||||
| ↳ `hs_pipeline_stage` | string | Current pipeline stage |
|
||||
| ↳ `hs_ticket_priority` | string | Ticket priority \(LOW, MEDIUM, HIGH\) |
|
||||
| ↳ `hs_ticket_category` | string | Ticket category |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Ticket creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `ticketId` | string | The updated ticket ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_search_tickets`
|
||||
|
||||
Search for tickets in HubSpot using filters, sorting, and queries
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `filterGroups` | array | No | Array of filter groups as JSON. Each group contains "filters" array with objects having "propertyName", "operator" \(e.g., "EQ", "NEQ", "CONTAINS_TOKEN", "NOT_CONTAINS_TOKEN"\), and "value" |
|
||||
| `sorts` | array | No | Array of sort objects as JSON with "propertyName" and "direction" \("ASCENDING" or "DESCENDING"\) |
|
||||
| `query` | string | No | Search query string to match against ticket subject and other text fields |
|
||||
| `properties` | array | No | Array of HubSpot property names to return \(e.g., \["subject", "content", "hs_ticket_priority"\]\) |
|
||||
| `limit` | number | No | Maximum number of results to return \(max 200\) |
|
||||
| `after` | string | No | Pagination cursor for next page \(from previous response\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tickets` | array | Array of HubSpot ticket records |
|
||||
| ↳ `subject` | string | Ticket subject/name |
|
||||
| ↳ `content` | string | Ticket content/description |
|
||||
| ↳ `hs_pipeline` | string | Pipeline the ticket is in |
|
||||
| ↳ `hs_pipeline_stage` | string | Current pipeline stage |
|
||||
| ↳ `hs_ticket_priority` | string | Ticket priority \(LOW, MEDIUM, HIGH\) |
|
||||
| ↳ `hs_ticket_category` | string | Ticket category |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Ticket creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| `total` | number | Total number of matching tickets |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_list_line_items`
|
||||
|
||||
Retrieve all line items from HubSpot account with pagination support
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
|
||||
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "name,quantity,price,amount"\) |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "deals,quotes"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `lineItems` | array | Array of HubSpot line item records |
|
||||
| ↳ `name` | string | Line item name |
|
||||
| ↳ `description` | string | Full description of the product |
|
||||
| ↳ `hs_sku` | string | Unique product identifier \(SKU\) |
|
||||
| ↳ `quantity` | string | Number of units included |
|
||||
| ↳ `price` | string | Unit price |
|
||||
| ↳ `amount` | string | Total cost \(quantity * unit price\) |
|
||||
| ↳ `hs_line_item_currency_code` | string | Currency code |
|
||||
| ↳ `recurringbillingfrequency` | string | Recurring billing frequency |
|
||||
| ↳ `hs_recurring_billing_start_date` | string | Recurring billing start date |
|
||||
| ↳ `hs_recurring_billing_end_date` | string | Recurring billing end date |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_get_line_item`
|
||||
|
||||
Retrieve a single line item by ID from HubSpot
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `lineItemId` | string | Yes | The HubSpot line item ID to retrieve |
|
||||
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "name,quantity,price,amount"\) |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "deals,quotes"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `lineItem` | object | HubSpot line item record |
|
||||
| ↳ `name` | string | Line item name |
|
||||
| ↳ `description` | string | Full description of the product |
|
||||
| ↳ `hs_sku` | string | Unique product identifier \(SKU\) |
|
||||
| ↳ `quantity` | string | Number of units included |
|
||||
| ↳ `price` | string | Unit price |
|
||||
| ↳ `amount` | string | Total cost \(quantity * unit price\) |
|
||||
| ↳ `hs_line_item_currency_code` | string | Currency code |
|
||||
| ↳ `recurringbillingfrequency` | string | Recurring billing frequency |
|
||||
| ↳ `hs_recurring_billing_start_date` | string | Recurring billing start date |
|
||||
| ↳ `hs_recurring_billing_end_date` | string | Recurring billing end date |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `lineItemId` | string | The retrieved line item ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_create_line_item`
|
||||
|
||||
Create a new line item in HubSpot. Requires at least a name property
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `properties` | object | Yes | Line item properties as JSON object \(e.g., \{"name": "Product A", "quantity": "2", "price": "50.00", "hs_sku": "SKU-001"\}\) |
|
||||
| `associations` | array | No | Array of associations to create with the line item as JSON. Each object should have "to.id" and "types" array with "associationCategory" and "associationTypeId" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `lineItem` | object | HubSpot line item record |
|
||||
| ↳ `name` | string | Line item name |
|
||||
| ↳ `description` | string | Full description of the product |
|
||||
| ↳ `hs_sku` | string | Unique product identifier \(SKU\) |
|
||||
| ↳ `quantity` | string | Number of units included |
|
||||
| ↳ `price` | string | Unit price |
|
||||
| ↳ `amount` | string | Total cost \(quantity * unit price\) |
|
||||
| ↳ `hs_line_item_currency_code` | string | Currency code |
|
||||
| ↳ `recurringbillingfrequency` | string | Recurring billing frequency |
|
||||
| ↳ `hs_recurring_billing_start_date` | string | Recurring billing start date |
|
||||
| ↳ `hs_recurring_billing_end_date` | string | Recurring billing end date |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `lineItemId` | string | The created line item ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_update_line_item`
|
||||
|
||||
Update an existing line item in HubSpot by ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `lineItemId` | string | Yes | The HubSpot line item ID to update |
|
||||
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
|
||||
| `properties` | object | Yes | Line item properties to update as JSON object \(e.g., \{"quantity": "5", "price": "25.00"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `lineItem` | object | HubSpot line item record |
|
||||
| ↳ `name` | string | Line item name |
|
||||
| ↳ `description` | string | Full description of the product |
|
||||
| ↳ `hs_sku` | string | Unique product identifier \(SKU\) |
|
||||
| ↳ `quantity` | string | Number of units included |
|
||||
| ↳ `price` | string | Unit price |
|
||||
| ↳ `amount` | string | Total cost \(quantity * unit price\) |
|
||||
| ↳ `hs_line_item_currency_code` | string | Currency code |
|
||||
| ↳ `recurringbillingfrequency` | string | Recurring billing frequency |
|
||||
| ↳ `hs_recurring_billing_start_date` | string | Recurring billing start date |
|
||||
| ↳ `hs_recurring_billing_end_date` | string | Recurring billing end date |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `lineItemId` | string | The updated line item ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_list_quotes`
|
||||
|
||||
Retrieve all quotes from HubSpot account with pagination support
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
|
||||
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "hs_title,hs_expiration_date,hs_status"\) |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "deals,line_items"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `quotes` | array | Array of HubSpot quote records |
|
||||
| ↳ `hs_title` | string | Quote name/title |
|
||||
| ↳ `hs_expiration_date` | string | Expiration date |
|
||||
| ↳ `hs_status` | string | Quote status |
|
||||
| ↳ `hs_esign_enabled` | string | Whether e-signatures are enabled |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_get_quote`
|
||||
|
||||
Retrieve a single quote by ID from HubSpot
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `quoteId` | string | Yes | The HubSpot quote ID to retrieve |
|
||||
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "hs_title,hs_expiration_date,hs_status"\) |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "deals,line_items"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `quote` | object | HubSpot quote record |
|
||||
| ↳ `hs_title` | string | Quote name/title |
|
||||
| ↳ `hs_expiration_date` | string | Expiration date |
|
||||
| ↳ `hs_status` | string | Quote status |
|
||||
| ↳ `hs_esign_enabled` | string | Whether e-signatures are enabled |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `createdate` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `quoteId` | string | The retrieved quote ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_list_appointments`
|
||||
|
||||
Retrieve all appointments from HubSpot account with pagination support
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
|
||||
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "hs_meeting_title,hs_meeting_start_time"\) |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `appointments` | array | Array of HubSpot appointment records |
|
||||
| ↳ `hs_appointment_type` | string | Appointment type |
|
||||
| ↳ `hs_meeting_title` | string | Meeting title |
|
||||
| ↳ `hs_meeting_start_time` | string | Start time \(ISO 8601\) |
|
||||
| ↳ `hs_meeting_end_time` | string | End time \(ISO 8601\) |
|
||||
| ↳ `hs_meeting_location` | string | Meeting location |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `hs_createdate` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_get_appointment`
|
||||
|
||||
Retrieve a single appointment by ID from HubSpot
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `appointmentId` | string | Yes | The HubSpot appointment ID to retrieve |
|
||||
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return \(e.g., "hs_meeting_title,hs_meeting_start_time"\) |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for \(e.g., "contacts,companies"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `appointment` | object | HubSpot appointment record |
|
||||
| ↳ `hs_appointment_type` | string | Appointment type |
|
||||
| ↳ `hs_meeting_title` | string | Meeting title |
|
||||
| ↳ `hs_meeting_start_time` | string | Start time \(ISO 8601\) |
|
||||
| ↳ `hs_meeting_end_time` | string | End time \(ISO 8601\) |
|
||||
| ↳ `hs_meeting_location` | string | Meeting location |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `hs_createdate` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `appointmentId` | string | The retrieved appointment ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_create_appointment`
|
||||
|
||||
Create a new appointment in HubSpot
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `properties` | object | Yes | Appointment properties as JSON object \(e.g., \{"hs_meeting_title": "Discovery Call", "hs_meeting_start_time": "2024-01-15T10:00:00Z", "hs_meeting_end_time": "2024-01-15T11:00:00Z"\}\) |
|
||||
| `associations` | array | No | Array of associations to create with the appointment as JSON. Each object should have "to.id" and "types" array with "associationCategory" and "associationTypeId" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `appointment` | object | HubSpot appointment record |
|
||||
| ↳ `hs_appointment_type` | string | Appointment type |
|
||||
| ↳ `hs_meeting_title` | string | Meeting title |
|
||||
| ↳ `hs_meeting_start_time` | string | Start time \(ISO 8601\) |
|
||||
| ↳ `hs_meeting_end_time` | string | End time \(ISO 8601\) |
|
||||
| ↳ `hs_meeting_location` | string | Meeting location |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `hs_createdate` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `appointmentId` | string | The created appointment ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_update_appointment`
|
||||
|
||||
Update an existing appointment in HubSpot by ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `appointmentId` | string | Yes | The HubSpot appointment ID to update |
|
||||
| `idProperty` | string | No | Property to use as unique identifier. If not specified, uses record ID |
|
||||
| `properties` | object | Yes | Appointment properties to update as JSON object \(e.g., \{"hs_meeting_title": "Updated Call", "hs_meeting_location": "Zoom"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `appointment` | object | HubSpot appointment record |
|
||||
| ↳ `hs_appointment_type` | string | Appointment type |
|
||||
| ↳ `hs_meeting_title` | string | Meeting title |
|
||||
| ↳ `hs_meeting_start_time` | string | Start time \(ISO 8601\) |
|
||||
| ↳ `hs_meeting_end_time` | string | End time \(ISO 8601\) |
|
||||
| ↳ `hs_meeting_location` | string | Meeting location |
|
||||
| ↳ `hubspot_owner_id` | string | HubSpot owner ID |
|
||||
| ↳ `hs_object_id` | string | HubSpot object ID \(same as record ID\) |
|
||||
| ↳ `hs_createdate` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `hs_lastmodifieddate` | string | Last modified date \(ISO 8601\) |
|
||||
| `appointmentId` | string | The updated appointment ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_list_carts`
|
||||
|
||||
Retrieve all carts from HubSpot account with pagination support
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
|
||||
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `carts` | array | Array of HubSpot CRM records |
|
||||
| ↳ `id` | string | Unique record ID \(hs_object_id\) |
|
||||
| ↳ `createdAt` | string | Record creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Record last updated timestamp \(ISO 8601\) |
|
||||
| ↳ `archived` | boolean | Whether the record is archived |
|
||||
| ↳ `properties` | object | Record properties |
|
||||
| ↳ `associations` | object | Associated records |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_get_cart`
|
||||
|
||||
Retrieve a single cart by ID from HubSpot
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `cartId` | string | Yes | The HubSpot cart ID to retrieve |
|
||||
| `properties` | string | No | Comma-separated list of HubSpot property names to return |
|
||||
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `cart` | object | HubSpot CRM record |
|
||||
| ↳ `id` | string | Unique record ID \(hs_object_id\) |
|
||||
| ↳ `createdAt` | string | Record creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Record last updated timestamp \(ISO 8601\) |
|
||||
| ↳ `archived` | boolean | Whether the record is archived |
|
||||
| ↳ `properties` | object | Record properties |
|
||||
| ↳ `associations` | object | Associated records |
|
||||
| `cartId` | string | The retrieved cart ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_list_owners`
|
||||
|
||||
Retrieve all owners from HubSpot account with pagination support
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | string | No | Maximum number of results per page \(max 100, default 100\) |
|
||||
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
|
||||
| `email` | string | No | Filter owners by email address |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `owners` | array | Array of HubSpot owner objects |
|
||||
| ↳ `id` | string | Owner ID |
|
||||
| ↳ `email` | string | Owner email address |
|
||||
| ↳ `firstName` | string | Owner first name |
|
||||
| ↳ `lastName` | string | Owner last name |
|
||||
| ↳ `userId` | number | Associated user ID |
|
||||
| ↳ `teams` | array | Teams the owner belongs to |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
|
||||
| ↳ `archived` | boolean | Whether the owner is archived |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_list_marketing_events`
|
||||
|
||||
Retrieve all marketing events from HubSpot account with pagination support
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `limit` | string | No | Maximum number of results per page \(max 100, default 10\) |
|
||||
| `after` | string | No | Pagination cursor for next page of results \(from previous response\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `events` | array | Array of HubSpot marketing event objects |
|
||||
| ↳ `objectId` | string | Unique event ID \(HubSpot internal\) |
|
||||
| ↳ `eventName` | string | Event name |
|
||||
| ↳ `eventType` | string | Event type |
|
||||
| ↳ `eventStatus` | string | Event status |
|
||||
| ↳ `eventDescription` | string | Event description |
|
||||
| ↳ `eventUrl` | string | Event URL |
|
||||
| ↳ `eventOrganizer` | string | Event organizer |
|
||||
| ↳ `startDateTime` | string | Start date/time \(ISO 8601\) |
|
||||
| ↳ `endDateTime` | string | End date/time \(ISO 8601\) |
|
||||
| ↳ `eventCancelled` | boolean | Whether event is cancelled |
|
||||
| ↳ `eventCompleted` | boolean | Whether event is completed |
|
||||
| ↳ `registrants` | number | Number of registrants |
|
||||
| ↳ `attendees` | number | Number of attendees |
|
||||
| ↳ `cancellations` | number | Number of cancellations |
|
||||
| ↳ `noShows` | number | Number of no-shows |
|
||||
| ↳ `externalEventId` | string | External event ID |
|
||||
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_get_marketing_event`
|
||||
|
||||
Retrieve a single marketing event by ID from HubSpot
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `eventId` | string | Yes | The HubSpot marketing event objectId to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `event` | object | HubSpot marketing event |
|
||||
| ↳ `objectId` | string | Unique event ID \(HubSpot internal\) |
|
||||
| ↳ `eventName` | string | Event name |
|
||||
| ↳ `eventType` | string | Event type |
|
||||
| ↳ `eventStatus` | string | Event status |
|
||||
| ↳ `eventDescription` | string | Event description |
|
||||
| ↳ `eventUrl` | string | Event URL |
|
||||
| ↳ `eventOrganizer` | string | Event organizer |
|
||||
| ↳ `startDateTime` | string | Start date/time \(ISO 8601\) |
|
||||
| ↳ `endDateTime` | string | End date/time \(ISO 8601\) |
|
||||
| ↳ `eventCancelled` | boolean | Whether event is cancelled |
|
||||
| ↳ `eventCompleted` | boolean | Whether event is completed |
|
||||
| ↳ `registrants` | number | Number of registrants |
|
||||
| ↳ `attendees` | number | Number of attendees |
|
||||
| ↳ `cancellations` | number | Number of cancellations |
|
||||
| ↳ `noShows` | number | Number of no-shows |
|
||||
| ↳ `externalEventId` | string | External event ID |
|
||||
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
|
||||
| `eventId` | string | The retrieved marketing event ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_list_lists`
|
||||
|
||||
Search and retrieve lists from HubSpot account
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | No | Search query to filter lists by name. Leave empty to return all lists. |
|
||||
| `count` | string | No | Maximum number of results to return \(default 20, max 500\) |
|
||||
| `offset` | string | No | Pagination offset for next page of results \(use the offset value from previous response\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `lists` | array | Array of HubSpot list objects |
|
||||
| ↳ `listId` | string | List ID |
|
||||
| ↳ `name` | string | List name |
|
||||
| ↳ `objectTypeId` | string | Object type ID \(e.g., 0-1 for contacts\) |
|
||||
| ↳ `processingType` | string | Processing type \(MANUAL, DYNAMIC, SNAPSHOT\) |
|
||||
| ↳ `processingStatus` | string | Processing status \(COMPLETE, PROCESSING\) |
|
||||
| ↳ `listVersion` | number | List version number |
|
||||
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
|
||||
| `paging` | object | Pagination information for fetching more results |
|
||||
| ↳ `after` | string | Cursor for next page of results |
|
||||
| ↳ `link` | string | Link to next page |
|
||||
| `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records are available |
|
||||
| ↳ `total` | number | Total number of lists matching the query |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_get_list`
|
||||
|
||||
Retrieve a single list by ID from HubSpot
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `listId` | string | Yes | The HubSpot list ID to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `list` | object | HubSpot list |
|
||||
| ↳ `listId` | string | List ID |
|
||||
| ↳ `name` | string | List name |
|
||||
| ↳ `objectTypeId` | string | Object type ID \(e.g., 0-1 for contacts\) |
|
||||
| ↳ `processingType` | string | Processing type \(MANUAL, DYNAMIC, SNAPSHOT\) |
|
||||
| ↳ `processingStatus` | string | Processing status \(COMPLETE, PROCESSING\) |
|
||||
| ↳ `listVersion` | number | List version number |
|
||||
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
|
||||
| `listId` | string | The retrieved list ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `hubspot_create_list`
|
||||
|
||||
Create a new list in HubSpot. Specify the object type and processing type (MANUAL or DYNAMIC)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `name` | string | Yes | Name of the list |
|
||||
| `objectTypeId` | string | Yes | Object type ID \(e.g., "0-1" for contacts, "0-2" for companies\) |
|
||||
| `processingType` | string | Yes | Processing type: "MANUAL" for static lists or "DYNAMIC" for active lists |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `list` | object | HubSpot list |
|
||||
| ↳ `listId` | string | List ID |
|
||||
| ↳ `name` | string | List name |
|
||||
| ↳ `objectTypeId` | string | Object type ID \(e.g., 0-1 for contacts\) |
|
||||
| ↳ `processingType` | string | Processing type \(MANUAL, DYNAMIC, SNAPSHOT\) |
|
||||
| ↳ `processingStatus` | string | Processing status \(COMPLETE, PROCESSING\) |
|
||||
| ↳ `listVersion` | number | List version number |
|
||||
| ↳ `createdAt` | string | Creation date \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated date \(ISO 8601\) |
|
||||
| `listId` | string | The created list ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
|
||||
|
||||
149
apps/docs/content/docs/en/tools/ketch.mdx
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: Ketch
|
||||
description: Manage privacy consent, subscriptions, and data subject rights
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="ketch"
|
||||
color="#9B5CFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Ketch](https://www.ketch.com/) is an AI-powered privacy, consent, and data governance platform that helps organizations automate compliance with global privacy regulations. It provides tools for managing consent preferences, handling data subject rights requests, and controlling subscription communications.
|
||||
|
||||
With Ketch, you can:
|
||||
|
||||
- **Retrieve consent status**: Query the current consent preferences for any data subject across configured purposes and legal bases
|
||||
- **Update consent preferences**: Set or modify consent for specific purposes (e.g., analytics, marketing) with the appropriate legal basis (opt-in, opt-out, disclosure)
|
||||
- **Manage subscriptions**: Get and update subscription topic preferences and global controls across contact methods like email and SMS
|
||||
- **Invoke data subject rights**: Submit privacy rights requests including data access, deletion, correction, and processing restriction under regulations like GDPR and CCPA
|
||||
|
||||
To use Ketch, drop the Ketch block into your workflow and provide your organization code, property code, and environment code. The Ketch Web API is a public API — no API key or OAuth credentials are required. Identity is determined by the organization and property codes along with the data subject's identity (e.g., email address).
|
||||
|
||||
These capabilities let you automate privacy compliance workflows, respond to user consent changes in real time, and manage data subject rights requests as part of your broader automation pipelines.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Ketch into the workflow. Retrieve and update consent preferences, manage subscription topics and controls, and submit data subject rights requests for access, deletion, correction, or processing restriction.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `ketch_get_consent`
|
||||
|
||||
Retrieve consent status for a data subject. Returns the current consent preferences for each configured purpose.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `organizationCode` | string | Yes | Ketch organization code |
|
||||
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
|
||||
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
|
||||
| `jurisdictionCode` | string | No | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
|
||||
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
|
||||
| `purposes` | json | No | Optional purposes to filter the consent query |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `purposes` | object | Map of purpose codes to consent status and legal basis |
|
||||
| ↳ `allowed` | string | Consent status for the purpose: "granted" or "denied" |
|
||||
| ↳ `legalBasisCode` | string | Legal basis code \(e.g., "consent_optin", "consent_optout", "disclosure", "other"\) |
|
||||
| `vendors` | object | Map of vendor consent statuses |
|
||||
|
||||
### `ketch_set_consent`
|
||||
|
||||
Update consent preferences for a data subject. Sets the consent status for specified purposes with the appropriate legal basis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `organizationCode` | string | Yes | Ketch organization code |
|
||||
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
|
||||
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
|
||||
| `jurisdictionCode` | string | No | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
|
||||
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
|
||||
| `purposes` | json | Yes | Map of purpose codes to consent settings \(e.g., \{"analytics": \{"allowed": "granted", "legalBasisCode": "consent_optin"\}\}\) |
|
||||
| `collectedAt` | number | No | UNIX timestamp when consent was collected \(defaults to current time\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `purposes` | object | Updated consent status map of purpose codes to consent settings |
|
||||
| ↳ `allowed` | string | Consent status for the purpose: "granted" or "denied" |
|
||||
| ↳ `legalBasisCode` | string | Legal basis code \(e.g., "consent_optin", "consent_optout", "disclosure", "other"\) |
|
||||
|
||||
### `ketch_get_subscriptions`
|
||||
|
||||
Retrieve subscription preferences for a data subject. Returns the current subscription topic and control statuses.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `organizationCode` | string | Yes | Ketch organization code |
|
||||
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
|
||||
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
|
||||
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `topics` | object | Map of topic codes to contact method settings \(e.g., \{"newsletter": \{"email": \{"status": "granted"\}\}\}\) |
|
||||
| `controls` | object | Map of control codes to settings \(e.g., \{"global_unsubscribe": \{"status": "denied"\}\}\) |
|
||||
|
||||
### `ketch_set_subscriptions`
|
||||
|
||||
Update subscription preferences for a data subject. Sets topic and control statuses for email, SMS, and other contact methods.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `organizationCode` | string | Yes | Ketch organization code |
|
||||
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
|
||||
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
|
||||
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
|
||||
| `topics` | json | No | Map of topic codes to contact method settings \(e.g., \{"newsletter": \{"email": \{"status": "granted"\}, "sms": \{"status": "denied"\}\}\}\) |
|
||||
| `controls` | json | No | Map of control codes to settings \(e.g., \{"global_unsubscribe": \{"status": "denied"\}\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the subscription preferences were updated |
|
||||
|
||||
### `ketch_invoke_right`
|
||||
|
||||
Submit a data subject rights request (e.g., access, delete, correct, restrict processing). Initiates a privacy rights workflow in Ketch.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `organizationCode` | string | Yes | Ketch organization code |
|
||||
| `propertyCode` | string | Yes | Digital property code defined in Ketch |
|
||||
| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) |
|
||||
| `jurisdictionCode` | string | Yes | Jurisdiction code \(e.g., "gdpr", "ccpa"\) |
|
||||
| `rightCode` | string | Yes | Privacy right code to invoke \(e.g., "access", "delete", "correct", "restrict_processing"\) |
|
||||
| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) |
|
||||
| `userData` | json | No | Optional data subject information \(e.g., \{"email": "user@example.com", "firstName": "John", "lastName": "Doe"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the rights request was submitted |
|
||||
| `message` | string | Response message from Ketch |
|
||||
|
||||
|
||||
388
apps/docs/content/docs/en/tools/launchdarkly.mdx
Normal file
@@ -0,0 +1,388 @@
|
||||
---
|
||||
title: LaunchDarkly
|
||||
description: Manage feature flags with LaunchDarkly.
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="launchdarkly"
|
||||
color="#191919"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[LaunchDarkly](https://launchdarkly.com/) is a feature management platform that enables teams to safely deploy, control, and measure their software features at scale.
|
||||
|
||||
With the LaunchDarkly integration in Sim, you can:
|
||||
|
||||
- **Feature flag management** — List, create, update, toggle, and delete feature flags programmatically. Toggle flags on or off in specific environments using LaunchDarkly's semantic patch API.
|
||||
- **Flag status monitoring** — Check whether a flag is active, inactive, new, or launched in a given environment. Track the last time a flag was evaluated.
|
||||
- **Project and environment management** — List all projects and their environments to understand your LaunchDarkly organization structure.
|
||||
- **User segments** — List user segments within a project and environment to understand how your audience is organized for targeting.
|
||||
- **Team visibility** — List account members and their roles for auditing and access management workflows.
|
||||
- **Audit log** — Retrieve recent audit log entries to track who changed what, when. Filter entries by resource type for targeted monitoring.
|
||||
|
||||
In Sim, the LaunchDarkly integration enables your agents to automate feature flag operations as part of their workflows. This allows for automation scenarios such as toggling flags on/off based on deployment pipeline events, monitoring flag status and alerting on stale or unused flags, auditing flag changes by querying the audit log after deployments, syncing flag metadata with your project management tools, and listing all feature flags across projects for governance.
|
||||
|
||||
## Authentication
|
||||
|
||||
This integration uses a LaunchDarkly API key. You can create personal access tokens or service tokens in the LaunchDarkly dashboard under **Account Settings > Authorization**. The API key is passed directly in the `Authorization` header (no `Bearer` prefix).
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you encounter issues with the LaunchDarkly integration, contact us at [help@sim.ai](mailto:help@sim.ai)
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate LaunchDarkly into your workflow. List, create, update, toggle, and delete feature flags. Manage projects, environments, segments, members, and audit logs. Requires API Key.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `launchdarkly_create_flag`
|
||||
|
||||
Create a new feature flag in a LaunchDarkly project.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key to create the flag in |
|
||||
| `name` | string | Yes | Human-readable name for the feature flag |
|
||||
| `key` | string | Yes | Unique key for the feature flag \(used in code\) |
|
||||
| `description` | string | No | Description of the feature flag |
|
||||
| `tags` | string | No | Comma-separated list of tags |
|
||||
| `temporary` | boolean | No | Whether the flag is temporary \(default true\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The unique key of the feature flag |
|
||||
| `name` | string | The human-readable name of the feature flag |
|
||||
| `kind` | string | The type of flag \(boolean or multivariate\) |
|
||||
| `description` | string | Description of the feature flag |
|
||||
| `temporary` | boolean | Whether the flag is temporary |
|
||||
| `archived` | boolean | Whether the flag is archived |
|
||||
| `deprecated` | boolean | Whether the flag is deprecated |
|
||||
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
|
||||
| `tags` | array | Tags applied to the flag |
|
||||
| `variations` | array | The variations for this feature flag |
|
||||
| ↳ `value` | string | The variation value |
|
||||
| ↳ `name` | string | The variation name |
|
||||
| ↳ `description` | string | The variation description |
|
||||
| `maintainerId` | string | The ID of the member who maintains this flag |
|
||||
|
||||
### `launchdarkly_delete_flag`
|
||||
|
||||
Delete a feature flag from a LaunchDarkly project.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `flagKey` | string | Yes | The feature flag key to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the flag was successfully deleted |
|
||||
|
||||
### `launchdarkly_get_audit_log`
|
||||
|
||||
List audit log entries from your LaunchDarkly account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `limit` | number | No | Maximum number of entries to return \(default 10, max 20\) |
|
||||
| `spec` | string | No | Filter expression \(e.g., "resourceType:flag"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `entries` | array | List of audit log entries |
|
||||
| ↳ `id` | string | The audit log entry ID |
|
||||
| ↳ `date` | number | Unix timestamp in milliseconds |
|
||||
| ↳ `kind` | string | The type of action performed |
|
||||
| ↳ `name` | string | The name of the resource acted on |
|
||||
| ↳ `description` | string | Full description of the action |
|
||||
| ↳ `shortDescription` | string | Short description of the action |
|
||||
| ↳ `memberEmail` | string | Email of the member who performed the action |
|
||||
| ↳ `targetName` | string | Name of the target resource |
|
||||
| ↳ `targetKind` | string | Kind of the target resource |
|
||||
| `totalCount` | number | Total number of audit log entries |
|
||||
|
||||
### `launchdarkly_get_flag`
|
||||
|
||||
Get a single feature flag by key from a LaunchDarkly project.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `flagKey` | string | Yes | The feature flag key |
|
||||
| `environmentKey` | string | No | Filter flag configuration to a specific environment |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The unique key of the feature flag |
|
||||
| `name` | string | The human-readable name of the feature flag |
|
||||
| `kind` | string | The type of flag \(boolean or multivariate\) |
|
||||
| `description` | string | Description of the feature flag |
|
||||
| `temporary` | boolean | Whether the flag is temporary |
|
||||
| `archived` | boolean | Whether the flag is archived |
|
||||
| `deprecated` | boolean | Whether the flag is deprecated |
|
||||
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
|
||||
| `tags` | array | Tags applied to the flag |
|
||||
| `variations` | array | The variations for this feature flag |
|
||||
| ↳ `value` | string | The variation value |
|
||||
| ↳ `name` | string | The variation name |
|
||||
| ↳ `description` | string | The variation description |
|
||||
| `maintainerId` | string | The ID of the member who maintains this flag |
|
||||
| `on` | boolean | Whether the flag is on in the requested environment \(null if no single environment was specified\) |
|
||||
|
||||
### `launchdarkly_get_flag_status`
|
||||
|
||||
Get the status of a feature flag across environments (active, inactive, launched, etc.).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `flagKey` | string | Yes | The feature flag key |
|
||||
| `environmentKey` | string | Yes | The environment key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `name` | string | The flag status \(new, active, inactive, launched\) |
|
||||
| `lastRequested` | string | Timestamp of the last evaluation |
|
||||
| `defaultVal` | string | The default variation value |
|
||||
|
||||
### `launchdarkly_list_environments`
|
||||
|
||||
List environments in a LaunchDarkly project.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key to list environments for |
|
||||
| `limit` | number | No | Maximum number of environments to return \(default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `environments` | array | List of environments |
|
||||
| ↳ `id` | string | The environment ID |
|
||||
| ↳ `key` | string | The unique environment key |
|
||||
| ↳ `name` | string | The environment name |
|
||||
| ↳ `color` | string | The color assigned to this environment |
|
||||
| ↳ `apiKey` | string | The server-side SDK key for this environment |
|
||||
| ↳ `mobileKey` | string | The mobile SDK key for this environment |
|
||||
| ↳ `tags` | array | Tags applied to the environment |
|
||||
| `totalCount` | number | Total number of environments |
|
||||
|
||||
### `launchdarkly_list_flags`
|
||||
|
||||
List feature flags in a LaunchDarkly project.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key to list flags for |
|
||||
| `environmentKey` | string | No | Filter flag configurations to a specific environment |
|
||||
| `tag` | string | No | Filter flags by tag name |
|
||||
| `limit` | number | No | Maximum number of flags to return \(default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `flags` | array | List of feature flags |
|
||||
| ↳ `key` | string | The unique key of the feature flag |
|
||||
| ↳ `name` | string | The human-readable name of the feature flag |
|
||||
| ↳ `kind` | string | The type of flag \(boolean or multivariate\) |
|
||||
| ↳ `description` | string | Description of the feature flag |
|
||||
| ↳ `temporary` | boolean | Whether the flag is temporary |
|
||||
| ↳ `archived` | boolean | Whether the flag is archived |
|
||||
| ↳ `deprecated` | boolean | Whether the flag is deprecated |
|
||||
| ↳ `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
|
||||
| ↳ `tags` | array | Tags applied to the flag |
|
||||
| ↳ `variations` | array | The variations for this feature flag |
|
||||
| ↳ `value` | string | The variation value |
|
||||
| ↳ `name` | string | The variation name |
|
||||
| ↳ `description` | string | The variation description |
|
||||
| ↳ `maintainerId` | string | The ID of the member who maintains this flag |
|
||||
| `totalCount` | number | Total number of flags |
|
||||
|
||||
### `launchdarkly_list_members`
|
||||
|
||||
List account members in your LaunchDarkly organization.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `limit` | number | No | Maximum number of members to return \(default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `members` | array | List of account members |
|
||||
| ↳ `id` | string | The member ID |
|
||||
| ↳ `email` | string | The member email address |
|
||||
| ↳ `firstName` | string | The member first name |
|
||||
| ↳ `lastName` | string | The member last name |
|
||||
| ↳ `role` | string | The member role \(reader, writer, admin, owner\) |
|
||||
| ↳ `lastSeen` | number | Unix timestamp of last activity |
|
||||
| ↳ `creationDate` | number | Unix timestamp when the member was created |
|
||||
| ↳ `verified` | boolean | Whether the member email is verified |
|
||||
| `totalCount` | number | Total number of members |
|
||||
|
||||
### `launchdarkly_list_projects`
|
||||
|
||||
List all projects in your LaunchDarkly account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `limit` | number | No | Maximum number of projects to return \(default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projects` | array | List of projects |
|
||||
| ↳ `id` | string | The project ID |
|
||||
| ↳ `key` | string | The unique project key |
|
||||
| ↳ `name` | string | The project name |
|
||||
| ↳ `tags` | array | Tags applied to the project |
|
||||
| `totalCount` | number | Total number of projects |
|
||||
|
||||
### `launchdarkly_list_segments`
|
||||
|
||||
List user segments in a LaunchDarkly project and environment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `environmentKey` | string | Yes | The environment key |
|
||||
| `limit` | number | No | Maximum number of segments to return \(default 20\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `segments` | array | List of user segments |
|
||||
| ↳ `key` | string | The unique segment key |
|
||||
| ↳ `name` | string | The segment name |
|
||||
| ↳ `description` | string | The segment description |
|
||||
| ↳ `tags` | array | Tags applied to the segment |
|
||||
| ↳ `creationDate` | number | Unix timestamp in milliseconds when the segment was created |
|
||||
| ↳ `unbounded` | boolean | Whether this is an unbounded \(big\) segment |
|
||||
| ↳ `included` | array | User keys explicitly included in the segment |
|
||||
| ↳ `excluded` | array | User keys explicitly excluded from the segment |
|
||||
| `totalCount` | number | Total number of segments |
|
||||
|
||||
### `launchdarkly_toggle_flag`
|
||||
|
||||
Toggle a feature flag on or off in a specific LaunchDarkly environment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `flagKey` | string | Yes | The feature flag key to toggle |
|
||||
| `environmentKey` | string | Yes | The environment key to toggle the flag in |
|
||||
| `enabled` | boolean | Yes | Whether to turn the flag on \(true\) or off \(false\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The unique key of the feature flag |
|
||||
| `name` | string | The human-readable name of the feature flag |
|
||||
| `kind` | string | The type of flag \(boolean or multivariate\) |
|
||||
| `description` | string | Description of the feature flag |
|
||||
| `temporary` | boolean | Whether the flag is temporary |
|
||||
| `archived` | boolean | Whether the flag is archived |
|
||||
| `deprecated` | boolean | Whether the flag is deprecated |
|
||||
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
|
||||
| `tags` | array | Tags applied to the flag |
|
||||
| `variations` | array | The variations for this feature flag |
|
||||
| ↳ `value` | string | The variation value |
|
||||
| ↳ `name` | string | The variation name |
|
||||
| ↳ `description` | string | The variation description |
|
||||
| `maintainerId` | string | The ID of the member who maintains this flag |
|
||||
| `on` | boolean | Whether the flag is now on in the target environment |
|
||||
|
||||
### `launchdarkly_update_flag`
|
||||
|
||||
Update a feature flag metadata (name, description, tags, temporary, archived) using semantic patch.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | LaunchDarkly API key |
|
||||
| `projectKey` | string | Yes | The project key |
|
||||
| `flagKey` | string | Yes | The feature flag key to update |
|
||||
| `updateName` | string | No | New name for the flag |
|
||||
| `updateDescription` | string | No | New description for the flag |
|
||||
| `addTags` | string | No | Comma-separated tags to add |
|
||||
| `removeTags` | string | No | Comma-separated tags to remove |
|
||||
| `archive` | boolean | No | Set to true to archive, false to restore |
|
||||
| `comment` | string | No | Optional comment explaining the update |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `key` | string | The unique key of the feature flag |
|
||||
| `name` | string | The human-readable name of the feature flag |
|
||||
| `kind` | string | The type of flag \(boolean or multivariate\) |
|
||||
| `description` | string | Description of the feature flag |
|
||||
| `temporary` | boolean | Whether the flag is temporary |
|
||||
| `archived` | boolean | Whether the flag is archived |
|
||||
| `deprecated` | boolean | Whether the flag is deprecated |
|
||||
| `creationDate` | number | Unix timestamp in milliseconds when the flag was created |
|
||||
| `tags` | array | Tags applied to the flag |
|
||||
| `variations` | array | The variations for this feature flag |
|
||||
| ↳ `value` | string | The variation value |
|
||||
| ↳ `name` | string | The variation name |
|
||||
| ↳ `description` | string | The variation description |
|
||||
| `maintainerId` | string | The ID of the member who maintains this flag |
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"pages": [
|
||||
"index",
|
||||
"a2a",
|
||||
"agentmail",
|
||||
"ahrefs",
|
||||
"airtable",
|
||||
"airweave",
|
||||
@@ -39,6 +40,7 @@
|
||||
"enrich",
|
||||
"evernote",
|
||||
"exa",
|
||||
"extend",
|
||||
"fathom",
|
||||
"file",
|
||||
"firecrawl",
|
||||
@@ -68,6 +70,7 @@
|
||||
"google_vault",
|
||||
"grafana",
|
||||
"grain",
|
||||
"granola",
|
||||
"greenhouse",
|
||||
"greptile",
|
||||
"hex",
|
||||
@@ -83,8 +86,10 @@
|
||||
"jira",
|
||||
"jira_service_management",
|
||||
"kalshi",
|
||||
"ketch",
|
||||
"knowledge",
|
||||
"langsmith",
|
||||
"launchdarkly",
|
||||
"lemlist",
|
||||
"linear",
|
||||
"linkedin",
|
||||
@@ -119,6 +124,7 @@
|
||||
"polymarket",
|
||||
"postgresql",
|
||||
"posthog",
|
||||
"profound",
|
||||
"pulse",
|
||||
"qdrant",
|
||||
"quiver",
|
||||
@@ -128,9 +134,12 @@
|
||||
"reducto",
|
||||
"resend",
|
||||
"revenuecat",
|
||||
"rippling",
|
||||
"rootly",
|
||||
"s3",
|
||||
"salesforce",
|
||||
"search",
|
||||
"secrets_manager",
|
||||
"sendgrid",
|
||||
"sentry",
|
||||
"serper",
|
||||
@@ -148,6 +157,7 @@
|
||||
"stt",
|
||||
"supabase",
|
||||
"table",
|
||||
"tailscale",
|
||||
"tavily",
|
||||
"telegram",
|
||||
"textract",
|
||||
|
||||
626
apps/docs/content/docs/en/tools/profound.mdx
Normal file
@@ -0,0 +1,626 @@
|
||||
---
|
||||
title: Profound
|
||||
description: AI visibility and analytics with Profound
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="profound"
|
||||
color="#000000"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Profound](https://tryprofound.com/) is an AI visibility and analytics platform that helps brands understand how they appear across AI-powered search engines, chatbots, and assistants. It tracks mentions, citations, sentiment, bot traffic, and referral patterns across platforms like ChatGPT, Perplexity, Google AI Overviews, and more.
|
||||
|
||||
With the Profound integration in Sim, you can:
|
||||
|
||||
- **Monitor AI Visibility**: Track share of voice, visibility scores, and mention counts across AI platforms for your brand and competitors.
|
||||
- **Analyze Sentiment**: Measure how positively or negatively your brand is discussed in AI-generated responses.
|
||||
- **Track Citations**: See which URLs are being cited by AI models and your citation share relative to competitors.
|
||||
- **Monitor Bot Traffic**: Analyze AI crawler activity on your domain, including GPTBot, ClaudeBot, and other AI agents, with hourly granularity.
|
||||
- **Track Referral Traffic**: Monitor human visits arriving from AI platforms to your website.
|
||||
- **Explore Prompt Data**: Access raw prompt-answer pairs, query fanouts, and prompt volume trends across AI platforms.
|
||||
- **Optimize Content**: Get AEO (Answer Engine Optimization) scores and actionable recommendations to improve how AI models reference your content.
|
||||
- **Manage Categories & Assets**: List and explore your tracked categories, assets (brands), topics, tags, personas, and regions.
|
||||
|
||||
These tools let your agents automate AI visibility monitoring, competitive intelligence, and content optimization workflows. To use the Profound integration, you'll need a Profound account with API access.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Track how your brand appears across AI platforms. Monitor visibility scores, sentiment, citations, bot traffic, referrals, content optimization, and prompt volumes with Profound.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `profound_list_categories`
|
||||
|
||||
List all organization categories in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `categories` | json | List of organization categories |
|
||||
| ↳ `id` | string | Category ID |
|
||||
| ↳ `name` | string | Category name |
|
||||
|
||||
### `profound_list_regions`
|
||||
|
||||
List all organization regions in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `regions` | json | List of organization regions |
|
||||
| ↳ `id` | string | Region ID \(UUID\) |
|
||||
| ↳ `name` | string | Region name |
|
||||
|
||||
### `profound_list_models`
|
||||
|
||||
List all AI models/platforms tracked in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `models` | json | List of AI models/platforms |
|
||||
| ↳ `id` | string | Model ID \(UUID\) |
|
||||
| ↳ `name` | string | Model/platform name |
|
||||
|
||||
### `profound_list_domains`
|
||||
|
||||
List all organization domains in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `domains` | json | List of organization domains |
|
||||
| ↳ `id` | string | Domain ID \(UUID\) |
|
||||
| ↳ `name` | string | Domain name |
|
||||
| ↳ `createdAt` | string | When the domain was added |
|
||||
|
||||
### `profound_list_assets`
|
||||
|
||||
List all organization assets (companies/brands) across all categories in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `assets` | json | List of organization assets with category info |
|
||||
| ↳ `id` | string | Asset ID |
|
||||
| ↳ `name` | string | Asset/company name |
|
||||
| ↳ `website` | string | Asset website URL |
|
||||
| ↳ `alternateDomains` | json | Alternate domain names |
|
||||
| ↳ `isOwned` | boolean | Whether this asset is owned by the organization |
|
||||
| ↳ `createdAt` | string | When the asset was created |
|
||||
| ↳ `logoUrl` | string | URL of the asset logo |
|
||||
| ↳ `categoryId` | string | Category ID the asset belongs to |
|
||||
| ↳ `categoryName` | string | Category name |
|
||||
|
||||
### `profound_list_personas`
|
||||
|
||||
List all organization personas across all categories in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `personas` | json | List of organization personas with profile details |
|
||||
| ↳ `id` | string | Persona ID |
|
||||
| ↳ `name` | string | Persona name |
|
||||
| ↳ `categoryId` | string | Category ID |
|
||||
| ↳ `categoryName` | string | Category name |
|
||||
| ↳ `persona` | json | Persona profile with behavior, employment, and demographics |
|
||||
|
||||
### `profound_category_topics`
|
||||
|
||||
List topics for a specific category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `topics` | json | List of topics in the category |
|
||||
| ↳ `id` | string | Topic ID \(UUID\) |
|
||||
| ↳ `name` | string | Topic name |
|
||||
|
||||
### `profound_category_tags`
|
||||
|
||||
List tags for a specific category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tags` | json | List of tags in the category |
|
||||
| ↳ `id` | string | Tag ID \(UUID\) |
|
||||
| ↳ `name` | string | Tag name |
|
||||
|
||||
### `profound_category_prompts`
|
||||
|
||||
List prompts for a specific category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 10000\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `orderDir` | string | No | Sort direction: asc or desc \(default desc\) |
|
||||
| `promptType` | string | No | Comma-separated prompt types to filter: visibility, sentiment |
|
||||
| `topicId` | string | No | Comma-separated topic IDs \(UUIDs\) to filter by |
|
||||
| `tagId` | string | No | Comma-separated tag IDs \(UUIDs\) to filter by |
|
||||
| `regionId` | string | No | Comma-separated region IDs \(UUIDs\) to filter by |
|
||||
| `platformId` | string | No | Comma-separated platform IDs \(UUIDs\) to filter by |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of prompts |
|
||||
| `nextCursor` | string | Cursor for next page of results |
|
||||
| `prompts` | json | List of prompts |
|
||||
| ↳ `id` | string | Prompt ID |
|
||||
| ↳ `prompt` | string | Prompt text |
|
||||
| ↳ `promptType` | string | Prompt type \(visibility or sentiment\) |
|
||||
| ↳ `topicId` | string | Topic ID |
|
||||
| ↳ `topicName` | string | Topic name |
|
||||
| ↳ `tags` | json | Associated tags |
|
||||
| ↳ `regions` | json | Associated regions |
|
||||
| ↳ `platforms` | json | Associated platforms |
|
||||
| ↳ `createdAt` | string | When the prompt was created |
|
||||
|
||||
### `profound_category_assets`
|
||||
|
||||
List assets (companies/brands) for a specific category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `assets` | json | List of assets in the category |
|
||||
| ↳ `id` | string | Asset ID |
|
||||
| ↳ `name` | string | Asset/company name |
|
||||
| ↳ `website` | string | Website URL |
|
||||
| ↳ `alternateDomains` | json | Alternate domain names |
|
||||
| ↳ `isOwned` | boolean | Whether the asset is owned by the organization |
|
||||
| ↳ `createdAt` | string | When the asset was created |
|
||||
| ↳ `logoUrl` | string | URL of the asset logo |
|
||||
|
||||
### `profound_category_personas`
|
||||
|
||||
List personas for a specific category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `personas` | json | List of personas in the category |
|
||||
| ↳ `id` | string | Persona ID |
|
||||
| ↳ `name` | string | Persona name |
|
||||
| ↳ `persona` | json | Persona profile with behavior, employment, and demographics |
|
||||
|
||||
### `profound_visibility_report`
|
||||
|
||||
Query AI visibility report for a category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: share_of_voice, mentions_count, visibility_score, executions, average_position |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: date, region, topic, model, asset_name, prompt, tag, persona |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"asset_name","operator":"is","value":"Company"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_sentiment_report`
|
||||
|
||||
Query sentiment report for a category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: positive, negative, occurrences |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: theme, date, region, topic, model, asset_name, tag, prompt, sentiment_type, persona |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"asset_name","operator":"is","value":"Company"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_citations_report`
|
||||
|
||||
Query citations report for a category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: count, citation_share |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: hostname, path, date, region, topic, model, tag, prompt, url, root_domain, persona, citation_category |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"hostname","operator":"is","value":"example.com"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_query_fanouts`
|
||||
|
||||
Query fanout report showing how AI models expand prompts into sub-queries in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: fanouts_per_execution, total_fanouts, share |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: prompt, query, model, region, date |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_prompt_answers`
|
||||
|
||||
Get raw prompt answers data for a category in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `categoryId` | string | Yes | Category ID \(UUID\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"prompt_type","operator":"is","value":"visibility"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of answer rows |
|
||||
| `data` | json | Raw prompt answer data |
|
||||
| ↳ `prompt` | string | The prompt text |
|
||||
| ↳ `promptType` | string | Prompt type \(visibility or sentiment\) |
|
||||
| ↳ `response` | string | AI model response text |
|
||||
| ↳ `mentions` | json | Companies/assets mentioned in the response |
|
||||
| ↳ `citations` | json | URLs cited in the response |
|
||||
| ↳ `topic` | string | Topic name |
|
||||
| ↳ `region` | string | Region name |
|
||||
| ↳ `model` | string | AI model/platform name |
|
||||
| ↳ `asset` | string | Asset name |
|
||||
| ↳ `createdAt` | string | Timestamp when the answer was collected |
|
||||
|
||||
### `profound_bots_report`
|
||||
|
||||
Query bot traffic report with hourly granularity for a domain in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `domain` | string | Yes | Domain to query bot traffic for \(e.g. example.com\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: count, citations, indexing, training, last_visit |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: date, hour, path, bot_name, bot_provider, bot_type |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"bot_name","operator":"is","value":"GPTBot"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_referrals_report`
|
||||
|
||||
Query human referral traffic report with hourly granularity for a domain in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `domain` | string | Yes | Domain to query referral traffic for \(e.g. example.com\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: visits, last_visit |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: date, hour, path, referral_source, referral_type |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"referral_source","operator":"is","value":"openai"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Report data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_raw_logs`
|
||||
|
||||
Get raw traffic logs with filters for a domain in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `domain` | string | Yes | Domain to query logs for \(e.g. example.com\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: timestamp, method, host, path, status_code, ip, user_agent, referer, bytes_sent, duration_ms, query_params |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"path","operator":"contains","value":"/blog"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of log entries |
|
||||
| `data` | json | Log data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values \(count\) |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_bot_logs`
|
||||
|
||||
Get identified bot visit logs with filters for a domain in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `domain` | string | Yes | Domain to query bot logs for \(e.g. example.com\) |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | No | End date \(YYYY-MM-DD or ISO 8601\). Defaults to now |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: timestamp, method, host, path, status_code, ip, user_agent, referer, bytes_sent, duration_ms, query_params, bot_name, bot_provider, bot_types |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"bot_name","operator":"is","value":"GPTBot"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of bot log entries |
|
||||
| `data` | json | Bot log data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values \(count\) |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_list_optimizations`
|
||||
|
||||
List content optimization entries for an asset in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `assetId` | string | Yes | Asset ID \(UUID\) |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
| `offset` | number | No | Offset for pagination \(default 0\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of optimization entries |
|
||||
| `optimizations` | json | List of content optimization entries |
|
||||
| ↳ `id` | string | Optimization ID \(UUID\) |
|
||||
| ↳ `title` | string | Content title |
|
||||
| ↳ `createdAt` | string | When the optimization was created |
|
||||
| ↳ `extractedInput` | string | Extracted input text |
|
||||
| ↳ `type` | string | Content type: file, text, or url |
|
||||
| ↳ `status` | string | Optimization status |
|
||||
|
||||
### `profound_optimization_analysis`
|
||||
|
||||
Get detailed content optimization analysis for a specific content item in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `assetId` | string | Yes | Asset ID \(UUID\) |
|
||||
| `contentId` | string | Yes | Content/optimization ID \(UUID\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | json | The analyzed content |
|
||||
| ↳ `format` | string | Content format: markdown or html |
|
||||
| ↳ `value` | string | Content text |
|
||||
| `aeoContentScore` | json | AEO content score with target zone |
|
||||
| ↳ `value` | number | AEO score value |
|
||||
| ↳ `targetZone` | json | Target zone range |
|
||||
| ↳ `low` | number | Low end of target range |
|
||||
| ↳ `high` | number | High end of target range |
|
||||
| `analysis` | json | Analysis breakdown by category |
|
||||
| ↳ `breakdown` | json | Array of scoring breakdowns |
|
||||
| ↳ `title` | string | Category title |
|
||||
| ↳ `weight` | number | Category weight |
|
||||
| ↳ `score` | number | Category score |
|
||||
| `recommendations` | json | Content optimization recommendations |
|
||||
| ↳ `title` | string | Recommendation title |
|
||||
| ↳ `status` | string | Status: done or pending |
|
||||
| ↳ `impact` | json | Impact details with section and score |
|
||||
| ↳ `suggestion` | json | Suggestion text and rationale |
|
||||
| ↳ `text` | string | Suggestion text |
|
||||
| ↳ `rationale` | string | Why this recommendation matters |
|
||||
|
||||
### `profound_prompt_volume`
|
||||
|
||||
Query prompt volume data to understand search demand across AI platforms in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `startDate` | string | Yes | Start date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `endDate` | string | Yes | End date \(YYYY-MM-DD or ISO 8601\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics: volume, change |
|
||||
| `dimensions` | string | No | Comma-separated dimensions: keyword, date, platform, country_code, matching_type, frequency |
|
||||
| `dateInterval` | string | No | Date interval: hour, day, week, month, year |
|
||||
| `filters` | string | No | JSON array of filter objects, e.g. \[\{"field":"keyword","operator":"contains","value":"best"\}\] |
|
||||
| `limit` | number | No | Maximum number of results \(default 10000, max 50000\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totalRows` | number | Total number of rows in the report |
|
||||
| `data` | json | Volume data rows with metrics and dimension values |
|
||||
| ↳ `metrics` | json | Array of metric values matching requested metrics order |
|
||||
| ↳ `dimensions` | json | Array of dimension values matching requested dimensions order |
|
||||
|
||||
### `profound_citation_prompts`
|
||||
|
||||
Get prompts that cite a specific domain across AI platforms in Profound
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Profound API Key |
|
||||
| `inputDomain` | string | Yes | Domain to look up citations for \(e.g. ramp.com\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Citation prompt data for the queried domain |
|
||||
|
||||
|
||||
@@ -59,8 +59,9 @@ Generate SVG images from text prompts using QuiverAI
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the SVG generation succeeded |
|
||||
| `output` | object | Generated SVG output |
|
||||
| ↳ `file` | file | Generated SVG file |
|
||||
| ↳ `svgContent` | string | Raw SVG markup content |
|
||||
| ↳ `file` | file | First generated SVG file |
|
||||
| ↳ `files` | json | All generated SVG files \(when n > 1\) |
|
||||
| ↳ `svgContent` | string | Raw SVG markup content of the first result |
|
||||
| ↳ `id` | string | Generation request ID |
|
||||
| ↳ `usage` | json | Token usage statistics |
|
||||
| ↳ `totalTokens` | number | Total tokens used |
|
||||
|
||||
2312
apps/docs/content/docs/en/tools/rippling.mdx
Normal file
891
apps/docs/content/docs/en/tools/rootly.mdx
Normal file
@@ -0,0 +1,891 @@
|
||||
---
|
||||
title: Rootly
|
||||
description: Manage incidents, alerts, and on-call with Rootly
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="rootly"
|
||||
color="#6C72C8"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Rootly](https://rootly.com/) is an incident management platform that helps teams respond to, mitigate, and learn from incidents — all without leaving Slack or your existing tools. Rootly automates on-call alerting, incident workflows, status page updates, and retrospectives so engineering teams can resolve issues faster and reduce toil.
|
||||
|
||||
**Why Rootly?**
|
||||
- **End-to-End Incident Management:** Create, track, update, and resolve incidents with full lifecycle support — from initial triage through retrospective.
|
||||
- **On-Call Alerting:** Create and manage alerts with deduplication, routing, and escalation to ensure the right people are notified immediately.
|
||||
- **Timeline Events:** Add structured timeline events to incidents for clear, auditable incident narratives.
|
||||
- **Service Catalog:** Maintain a catalog of services and map them to incidents for precise impact tracking.
|
||||
- **Severity & Prioritization:** Use configurable severity levels to prioritize incidents and drive appropriate response urgency.
|
||||
- **Retrospectives:** Access post-incident retrospectives to identify root causes, capture learnings, and drive reliability improvements.
|
||||
|
||||
**Using Rootly in Sim**
|
||||
|
||||
Sim's Rootly integration connects your agentic workflows directly to your Rootly account using an API key. With operations spanning incidents, alerts, services, severities, teams, environments, functionalities, incident types, and retrospectives, you can build powerful incident management automations without writing backend code.
|
||||
|
||||
**Key benefits of using Rootly in Sim:**
|
||||
- **Automated incident creation:** Trigger incident creation from monitoring alerts, customer reports, or anomaly detection workflows with full metadata including severity, services, and teams.
|
||||
- **Incident lifecycle automation:** Automatically update incident status, add timeline events, and attach mitigation or resolution messages as your response progresses.
|
||||
- **Alert management:** Create and list alerts with deduplication support to integrate Rootly into your existing monitoring and notification pipelines.
|
||||
- **Organizational awareness:** Query services, severities, teams, environments, functionalities, and incident types to build context-aware incident workflows.
|
||||
- **Retrospective insights:** List and filter retrospectives to feed post-incident learnings into continuous improvement workflows.
|
||||
|
||||
Whether you're automating incident response, building on-call alerting pipelines, or driving post-incident learning, Rootly in Sim gives you direct, secure access to the Rootly API — no middleware required. Simply configure your API key, select the operation you need, and let Sim handle the rest.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Rootly incident management into workflows. Create and manage incidents, alerts, services, severities, and retrospectives.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `rootly_create_incident`
|
||||
|
||||
Create a new incident in Rootly with optional severity, services, and teams.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `title` | string | No | The title of the incident \(auto-generated if not provided\) |
|
||||
| `summary` | string | No | A summary of the incident |
|
||||
| `severityId` | string | No | Severity ID to attach to the incident |
|
||||
| `status` | string | No | Incident status \(in_triage, started, detected, acknowledged, mitigated, resolved, closed, cancelled, scheduled, in_progress, completed\) |
|
||||
| `kind` | string | No | Incident kind \(normal, normal_sub, test, test_sub, example, example_sub, backfilled, scheduled, scheduled_sub\) |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs to attach |
|
||||
| `environmentIds` | string | No | Comma-separated environment IDs to attach |
|
||||
| `groupIds` | string | No | Comma-separated team/group IDs to attach |
|
||||
| `incidentTypeIds` | string | No | Comma-separated incident type IDs to attach |
|
||||
| `functionalityIds` | string | No | Comma-separated functionality IDs to attach |
|
||||
| `labels` | string | No | Labels as JSON object, e.g. \{"platform":"osx","version":"1.29"\} |
|
||||
| `private` | boolean | No | Create as a private incident \(cannot be undone\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incident` | object | The created incident |
|
||||
| ↳ `id` | string | Unique incident ID |
|
||||
| ↳ `sequentialId` | number | Sequential incident number |
|
||||
| ↳ `title` | string | Incident title |
|
||||
| ↳ `slug` | string | Incident slug |
|
||||
| ↳ `kind` | string | Incident kind |
|
||||
| ↳ `summary` | string | Incident summary |
|
||||
| ↳ `status` | string | Incident status |
|
||||
| ↳ `private` | boolean | Whether the incident is private |
|
||||
| ↳ `url` | string | URL to the incident |
|
||||
| ↳ `shortUrl` | string | Short URL to the incident |
|
||||
| ↳ `severityName` | string | Severity name |
|
||||
| ↳ `severityId` | string | Severity ID |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `mitigatedAt` | string | Mitigation date |
|
||||
| ↳ `resolvedAt` | string | Resolution date |
|
||||
| ↳ `closedAt` | string | Closed date |
|
||||
|
||||
### `rootly_get_incident`
|
||||
|
||||
Retrieve a single incident by ID from Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incident` | object | The incident details |
|
||||
| ↳ `id` | string | Unique incident ID |
|
||||
| ↳ `sequentialId` | number | Sequential incident number |
|
||||
| ↳ `title` | string | Incident title |
|
||||
| ↳ `slug` | string | Incident slug |
|
||||
| ↳ `kind` | string | Incident kind |
|
||||
| ↳ `summary` | string | Incident summary |
|
||||
| ↳ `status` | string | Incident status |
|
||||
| ↳ `private` | boolean | Whether the incident is private |
|
||||
| ↳ `url` | string | URL to the incident |
|
||||
| ↳ `shortUrl` | string | Short URL to the incident |
|
||||
| ↳ `severityName` | string | Severity name |
|
||||
| ↳ `severityId` | string | Severity ID |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `mitigatedAt` | string | Mitigation date |
|
||||
| ↳ `resolvedAt` | string | Resolution date |
|
||||
| ↳ `closedAt` | string | Closed date |
|
||||
|
||||
### `rootly_update_incident`
|
||||
|
||||
Update an existing incident in Rootly (status, severity, summary, etc.).
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to update |
|
||||
| `title` | string | No | Updated incident title |
|
||||
| `summary` | string | No | Updated incident summary |
|
||||
| `severityId` | string | No | Updated severity ID |
|
||||
| `status` | string | No | Updated status \(in_triage, started, detected, acknowledged, mitigated, resolved, closed, cancelled, scheduled, in_progress, completed\) |
|
||||
| `kind` | string | No | Incident kind \(normal, normal_sub, test, test_sub, example, example_sub, backfilled, scheduled, scheduled_sub\) |
|
||||
| `private` | boolean | No | Set incident as private \(cannot be undone\) |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs |
|
||||
| `environmentIds` | string | No | Comma-separated environment IDs |
|
||||
| `groupIds` | string | No | Comma-separated team/group IDs |
|
||||
| `incidentTypeIds` | string | No | Comma-separated incident type IDs to attach |
|
||||
| `functionalityIds` | string | No | Comma-separated functionality IDs to attach |
|
||||
| `labels` | string | No | Labels as JSON object, e.g. \{"platform":"osx","version":"1.29"\} |
|
||||
| `mitigationMessage` | string | No | How was the incident mitigated? |
|
||||
| `resolutionMessage` | string | No | How was the incident resolved? |
|
||||
| `cancellationMessage` | string | No | Why was the incident cancelled? |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incident` | object | The updated incident |
|
||||
| ↳ `id` | string | Unique incident ID |
|
||||
| ↳ `sequentialId` | number | Sequential incident number |
|
||||
| ↳ `title` | string | Incident title |
|
||||
| ↳ `slug` | string | Incident slug |
|
||||
| ↳ `kind` | string | Incident kind |
|
||||
| ↳ `summary` | string | Incident summary |
|
||||
| ↳ `status` | string | Incident status |
|
||||
| ↳ `private` | boolean | Whether the incident is private |
|
||||
| ↳ `url` | string | URL to the incident |
|
||||
| ↳ `shortUrl` | string | Short URL to the incident |
|
||||
| ↳ `severityName` | string | Severity name |
|
||||
| ↳ `severityId` | string | Severity ID |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `mitigatedAt` | string | Mitigation date |
|
||||
| ↳ `resolvedAt` | string | Resolution date |
|
||||
| ↳ `closedAt` | string | Closed date |
|
||||
|
||||
### `rootly_list_incidents`
|
||||
|
||||
List incidents from Rootly with optional filtering by status, severity, and more.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `status` | string | No | Filter by status \(in_triage, started, detected, acknowledged, mitigated, resolved, closed, cancelled, scheduled, in_progress, completed\) |
|
||||
| `severity` | string | No | Filter by severity slug |
|
||||
| `search` | string | No | Search term to filter incidents |
|
||||
| `services` | string | No | Filter by service slugs \(comma-separated\) |
|
||||
| `teams` | string | No | Filter by team slugs \(comma-separated\) |
|
||||
| `environments` | string | No | Filter by environment slugs \(comma-separated\) |
|
||||
| `sort` | string | No | Sort order \(e.g., -created_at, created_at, -started_at\) |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incidents` | array | List of incidents |
|
||||
| ↳ `id` | string | Unique incident ID |
|
||||
| ↳ `sequentialId` | number | Sequential incident number |
|
||||
| ↳ `title` | string | Incident title |
|
||||
| ↳ `slug` | string | Incident slug |
|
||||
| ↳ `kind` | string | Incident kind |
|
||||
| ↳ `summary` | string | Incident summary |
|
||||
| ↳ `status` | string | Incident status |
|
||||
| ↳ `private` | boolean | Whether the incident is private |
|
||||
| ↳ `url` | string | URL to the incident |
|
||||
| ↳ `shortUrl` | string | Short URL to the incident |
|
||||
| ↳ `severityName` | string | Severity name |
|
||||
| ↳ `severityId` | string | Severity ID |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `mitigatedAt` | string | Mitigation date |
|
||||
| ↳ `resolvedAt` | string | Resolution date |
|
||||
| ↳ `closedAt` | string | Closed date |
|
||||
| `totalCount` | number | Total number of incidents returned |
|
||||
|
||||
### `rootly_create_alert`
|
||||
|
||||
Create a new alert in Rootly for on-call notification and routing.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `summary` | string | Yes | The summary of the alert |
|
||||
| `description` | string | No | A detailed description of the alert |
|
||||
| `source` | string | Yes | The source of the alert \(e.g., api, manual, datadog, pagerduty\) |
|
||||
| `status` | string | No | Alert status on creation \(open, triggered\) |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs to attach |
|
||||
| `groupIds` | string | No | Comma-separated team/group IDs to attach |
|
||||
| `environmentIds` | string | No | Comma-separated environment IDs to attach |
|
||||
| `externalId` | string | No | External ID for the alert |
|
||||
| `externalUrl` | string | No | External URL for the alert |
|
||||
| `deduplicationKey` | string | No | Alerts sharing the same deduplication key are treated as a single alert |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The created alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_list_alerts`
|
||||
|
||||
List alerts from Rootly with optional filtering by status, source, and services.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `status` | string | No | Filter by status \(open, triggered, acknowledged, resolved\) |
|
||||
| `source` | string | No | Filter by source \(e.g., api, datadog, pagerduty\) |
|
||||
| `services` | string | No | Filter by service slugs \(comma-separated\) |
|
||||
| `environments` | string | No | Filter by environment slugs \(comma-separated\) |
|
||||
| `groups` | string | No | Filter by team/group slugs \(comma-separated\) |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alerts` | array | List of alerts |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
| `totalCount` | number | Total number of alerts returned |
|
||||
|
||||
### `rootly_add_incident_event`
|
||||
|
||||
Add a timeline event to an existing incident in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to add the event to |
|
||||
| `event` | string | Yes | The summary/description of the event |
|
||||
| `visibility` | string | No | Event visibility \(internal or external\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventId` | string | The ID of the created event |
|
||||
| `event` | string | The event summary |
|
||||
| `visibility` | string | Event visibility \(internal or external\) |
|
||||
| `occurredAt` | string | When the event occurred |
|
||||
| `createdAt` | string | Creation date |
|
||||
| `updatedAt` | string | Last update date |
|
||||
|
||||
### `rootly_list_services`
|
||||
|
||||
List services from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter services |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `services` | array | List of services |
|
||||
| ↳ `id` | string | Unique service ID |
|
||||
| ↳ `name` | string | Service name |
|
||||
| ↳ `slug` | string | Service slug |
|
||||
| ↳ `description` | string | Service description |
|
||||
| ↳ `color` | string | Service color |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of services returned |
|
||||
|
||||
### `rootly_list_severities`
|
||||
|
||||
List severity levels configured in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter severities |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `severities` | array | List of severity levels |
|
||||
| ↳ `id` | string | Unique severity ID |
|
||||
| ↳ `name` | string | Severity name |
|
||||
| ↳ `slug` | string | Severity slug |
|
||||
| ↳ `description` | string | Severity description |
|
||||
| ↳ `severity` | string | Severity level \(critical, high, medium, low\) |
|
||||
| ↳ `color` | string | Severity color |
|
||||
| ↳ `position` | number | Display position |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of severities returned |
|
||||
|
||||
### `rootly_list_teams`
|
||||
|
||||
List teams (groups) configured in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter teams |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `teams` | array | List of teams |
|
||||
| ↳ `id` | string | Unique team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `slug` | string | Team slug |
|
||||
| ↳ `description` | string | Team description |
|
||||
| ↳ `color` | string | Team color |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of teams returned |
|
||||
|
||||
### `rootly_list_environments`
|
||||
|
||||
List environments configured in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter environments |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `environments` | array | List of environments |
|
||||
| ↳ `id` | string | Unique environment ID |
|
||||
| ↳ `name` | string | Environment name |
|
||||
| ↳ `slug` | string | Environment slug |
|
||||
| ↳ `description` | string | Environment description |
|
||||
| ↳ `color` | string | Environment color |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of environments returned |
|
||||
|
||||
### `rootly_list_incident_types`
|
||||
|
||||
List incident types configured in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Filter incident types by name |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incidentTypes` | array | List of incident types |
|
||||
| ↳ `id` | string | Unique incident type ID |
|
||||
| ↳ `name` | string | Incident type name |
|
||||
| ↳ `slug` | string | Incident type slug |
|
||||
| ↳ `description` | string | Incident type description |
|
||||
| ↳ `color` | string | Incident type color |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of incident types returned |
|
||||
|
||||
### `rootly_list_functionalities`
|
||||
|
||||
List functionalities configured in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter functionalities |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `functionalities` | array | List of functionalities |
|
||||
| ↳ `id` | string | Unique functionality ID |
|
||||
| ↳ `name` | string | Functionality name |
|
||||
| ↳ `slug` | string | Functionality slug |
|
||||
| ↳ `description` | string | Functionality description |
|
||||
| ↳ `color` | string | Functionality color |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of functionalities returned |
|
||||
|
||||
### `rootly_list_retrospectives`
|
||||
|
||||
List incident retrospectives (post-mortems) from Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `status` | string | No | Filter by status \(draft, published\) |
|
||||
| `search` | string | No | Search term to filter retrospectives |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `retrospectives` | array | List of retrospectives |
|
||||
| ↳ `id` | string | Unique retrospective ID |
|
||||
| ↳ `title` | string | Retrospective title |
|
||||
| ↳ `status` | string | Status \(draft or published\) |
|
||||
| ↳ `url` | string | URL to the retrospective |
|
||||
| ↳ `startedAt` | string | Incident start date |
|
||||
| ↳ `mitigatedAt` | string | Mitigation date |
|
||||
| ↳ `resolvedAt` | string | Resolution date |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of retrospectives returned |
|
||||
|
||||
### `rootly_delete_incident`
|
||||
|
||||
Delete an incident by ID from Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the deletion succeeded |
|
||||
| `message` | string | Result message |
|
||||
|
||||
### `rootly_get_alert`
|
||||
|
||||
Retrieve a single alert by ID from Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The alert details |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_update_alert`
|
||||
|
||||
Update an existing alert in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to update |
|
||||
| `summary` | string | No | Updated alert summary |
|
||||
| `description` | string | No | Updated alert description |
|
||||
| `source` | string | No | Updated alert source |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs to attach |
|
||||
| `groupIds` | string | No | Comma-separated team/group IDs to attach |
|
||||
| `environmentIds` | string | No | Comma-separated environment IDs to attach |
|
||||
| `externalId` | string | No | Updated external ID |
|
||||
| `externalUrl` | string | No | Updated external URL |
|
||||
| `deduplicationKey` | string | No | Updated deduplication key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The updated alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_acknowledge_alert`
|
||||
|
||||
Acknowledge an alert in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to acknowledge |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The acknowledged alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_resolve_alert`
|
||||
|
||||
Resolve an alert in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `alertId` | string | Yes | The ID of the alert to resolve |
|
||||
| `resolutionMessage` | string | No | Message describing how the alert was resolved |
|
||||
| `resolveRelatedIncidents` | boolean | No | Whether to also resolve related incidents |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alert` | object | The resolved alert |
|
||||
| ↳ `id` | string | Unique alert ID |
|
||||
| ↳ `shortId` | string | Short alert ID |
|
||||
| ↳ `summary` | string | Alert summary |
|
||||
| ↳ `description` | string | Alert description |
|
||||
| ↳ `source` | string | Alert source |
|
||||
| ↳ `status` | string | Alert status |
|
||||
| ↳ `externalId` | string | External ID |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `deduplicationKey` | string | Deduplication key |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| ↳ `startedAt` | string | Start date |
|
||||
| ↳ `endedAt` | string | End date |
|
||||
|
||||
### `rootly_create_action_item`
|
||||
|
||||
Create a new action item for an incident in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to add the action item to |
|
||||
| `summary` | string | Yes | The title of the action item |
|
||||
| `description` | string | No | A detailed description of the action item |
|
||||
| `kind` | string | No | The kind of action item \(task, follow_up\) |
|
||||
| `priority` | string | No | Priority level \(high, medium, low\) |
|
||||
| `status` | string | No | Action item status \(open, in_progress, cancelled, done\) |
|
||||
| `assignedToUserId` | string | No | The user ID to assign the action item to |
|
||||
| `dueDate` | string | No | Due date for the action item |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `actionItem` | object | The created action item |
|
||||
| ↳ `id` | string | Unique action item ID |
|
||||
| ↳ `summary` | string | Action item title |
|
||||
| ↳ `description` | string | Action item description |
|
||||
| ↳ `kind` | string | Action item kind \(task, follow_up\) |
|
||||
| ↳ `priority` | string | Priority level |
|
||||
| ↳ `status` | string | Action item status |
|
||||
| ↳ `dueDate` | string | Due date |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
|
||||
### `rootly_list_action_items`
|
||||
|
||||
List action items for an incident in Rootly.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `incidentId` | string | Yes | The ID of the incident to list action items for |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `actionItems` | array | List of action items |
|
||||
| ↳ `id` | string | Unique action item ID |
|
||||
| ↳ `summary` | string | Action item title |
|
||||
| ↳ `description` | string | Action item description |
|
||||
| ↳ `kind` | string | Action item kind \(task, follow_up\) |
|
||||
| ↳ `priority` | string | Priority level |
|
||||
| ↳ `status` | string | Action item status |
|
||||
| ↳ `dueDate` | string | Due date |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of action items returned |
|
||||
|
||||
### `rootly_list_users`
|
||||
|
||||
List users from Rootly with optional search and email filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter users |
|
||||
| `email` | string | No | Filter users by email address |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | List of users |
|
||||
| ↳ `id` | string | Unique user ID |
|
||||
| ↳ `email` | string | User email address |
|
||||
| ↳ `firstName` | string | User first name |
|
||||
| ↳ `lastName` | string | User last name |
|
||||
| ↳ `fullName` | string | User full name |
|
||||
| ↳ `timeZone` | string | User time zone |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of users returned |
|
||||
|
||||
### `rootly_list_on_calls`
|
||||
|
||||
List current on-call entries from Rootly with optional filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `scheduleIds` | string | No | Comma-separated schedule IDs to filter by |
|
||||
| `escalationPolicyIds` | string | No | Comma-separated escalation policy IDs to filter by |
|
||||
| `userIds` | string | No | Comma-separated user IDs to filter by |
|
||||
| `serviceIds` | string | No | Comma-separated service IDs to filter by |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `onCalls` | array | List of on-call entries |
|
||||
| ↳ `id` | string | Unique on-call entry ID |
|
||||
| ↳ `userId` | string | ID of the on-call user |
|
||||
| ↳ `userName` | string | Name of the on-call user |
|
||||
| ↳ `scheduleId` | string | ID of the associated schedule |
|
||||
| ↳ `scheduleName` | string | Name of the associated schedule |
|
||||
| ↳ `escalationPolicyId` | string | ID of the associated escalation policy |
|
||||
| ↳ `startTime` | string | On-call start time |
|
||||
| ↳ `endTime` | string | On-call end time |
|
||||
| `totalCount` | number | Total number of on-call entries returned |
|
||||
|
||||
### `rootly_list_schedules`
|
||||
|
||||
List on-call schedules from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter schedules |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `schedules` | array | List of schedules |
|
||||
| ↳ `id` | string | Unique schedule ID |
|
||||
| ↳ `name` | string | Schedule name |
|
||||
| ↳ `description` | string | Schedule description |
|
||||
| ↳ `allTimeCoverage` | boolean | Whether schedule provides 24/7 coverage |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of schedules returned |
|
||||
|
||||
### `rootly_list_escalation_policies`
|
||||
|
||||
List escalation policies from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter escalation policies |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `escalationPolicies` | array | List of escalation policies |
|
||||
| ↳ `id` | string | Unique escalation policy ID |
|
||||
| ↳ `name` | string | Escalation policy name |
|
||||
| ↳ `description` | string | Escalation policy description |
|
||||
| ↳ `repeatCount` | number | Number of times to repeat escalation |
|
||||
| ↳ `groupIds` | array | Associated group IDs |
|
||||
| ↳ `serviceIds` | array | Associated service IDs |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of escalation policies returned |
|
||||
|
||||
### `rootly_list_causes`
|
||||
|
||||
List causes from Rootly with optional search filtering.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `search` | string | No | Search term to filter causes |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `causes` | array | List of causes |
|
||||
| ↳ `id` | string | Unique cause ID |
|
||||
| ↳ `name` | string | Cause name |
|
||||
| ↳ `slug` | string | Cause slug |
|
||||
| ↳ `description` | string | Cause description |
|
||||
| ↳ `position` | number | Cause position |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of causes returned |
|
||||
|
||||
### `rootly_list_playbooks`
|
||||
|
||||
List playbooks from Rootly with pagination support.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Rootly API key |
|
||||
| `pageSize` | number | No | Number of items per page \(default: 20\) |
|
||||
| `pageNumber` | number | No | Page number for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `playbooks` | array | List of playbooks |
|
||||
| ↳ `id` | string | Unique playbook ID |
|
||||
| ↳ `title` | string | Playbook title |
|
||||
| ↳ `summary` | string | Playbook summary |
|
||||
| ↳ `externalUrl` | string | External URL |
|
||||
| ↳ `createdAt` | string | Creation date |
|
||||
| ↳ `updatedAt` | string | Last update date |
|
||||
| `totalCount` | number | Total number of playbooks returned |
|
||||
|
||||
|
||||
157
apps/docs/content/docs/en/tools/secrets_manager.mdx
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
title: AWS Secrets Manager
|
||||
description: Connect to AWS Secrets Manager
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="secrets_manager"
|
||||
color="linear-gradient(45deg, #BD0816 0%, #FF5252 100%)"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) is a secrets management service that helps you protect access to your applications, services, and IT resources. It enables you to rotate, manage, and retrieve database credentials, API keys, and other secrets throughout their lifecycle.
|
||||
|
||||
With AWS Secrets Manager, you can:
|
||||
|
||||
- **Securely store secrets**: Encrypt secrets at rest using AWS KMS encryption keys
|
||||
- **Retrieve secrets programmatically**: Access secrets from your applications and workflows without hardcoding credentials
|
||||
- **Rotate secrets automatically**: Configure automatic rotation for supported services like RDS, Redshift, and DocumentDB
|
||||
- **Audit access**: Track secret access and changes through AWS CloudTrail integration
|
||||
- **Control access with IAM**: Use fine-grained IAM policies to manage who can access which secrets
|
||||
- **Replicate across regions**: Automatically replicate secrets to multiple AWS regions for disaster recovery
|
||||
|
||||
In Sim, the AWS Secrets Manager integration allows your workflows to securely retrieve credentials and configuration values at runtime, create and manage secrets as part of automation pipelines, and maintain a centralized secrets store that your agents can access. This is particularly useful for workflows that need to authenticate with external services, rotate credentials, or manage sensitive configuration across environments — all without exposing secrets in your workflow definitions.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate AWS Secrets Manager into the workflow. Can retrieve, create, update, list, and delete secrets.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `secrets_manager_get_secret`
|
||||
|
||||
Retrieve a secret value from AWS Secrets Manager
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `secretId` | string | Yes | The name or ARN of the secret to retrieve |
|
||||
| `versionId` | string | No | The unique identifier of the version to retrieve |
|
||||
| `versionStage` | string | No | The staging label of the version to retrieve \(e.g., AWSCURRENT, AWSPREVIOUS\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `name` | string | Name of the secret |
|
||||
| `secretValue` | string | The decrypted secret value |
|
||||
| `arn` | string | ARN of the secret |
|
||||
| `versionId` | string | Version ID of the secret |
|
||||
| `versionStages` | array | Staging labels attached to this version |
|
||||
| `createdDate` | string | Date the secret was created |
|
||||
|
||||
### `secrets_manager_list_secrets`
|
||||
|
||||
List secrets stored in AWS Secrets Manager
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `maxResults` | number | No | Maximum number of secrets to return \(1-100, default 100\) |
|
||||
| `nextToken` | string | No | Pagination token from a previous request |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `secrets` | json | List of secrets with name, ARN, description, and dates |
|
||||
| `nextToken` | string | Pagination token for the next page of results |
|
||||
| `count` | number | Number of secrets returned |
|
||||
|
||||
### `secrets_manager_create_secret`
|
||||
|
||||
Create a new secret in AWS Secrets Manager
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `name` | string | Yes | Name of the secret to create |
|
||||
| `secretValue` | string | Yes | The secret value \(plain text or JSON string\) |
|
||||
| `description` | string | No | Description of the secret |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `name` | string | Name of the created secret |
|
||||
| `arn` | string | ARN of the created secret |
|
||||
| `versionId` | string | Version ID of the created secret |
|
||||
|
||||
### `secrets_manager_update_secret`
|
||||
|
||||
Update the value of an existing secret in AWS Secrets Manager
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `secretId` | string | Yes | The name or ARN of the secret to update |
|
||||
| `secretValue` | string | Yes | The new secret value \(plain text or JSON string\) |
|
||||
| `description` | string | No | Updated description of the secret |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `name` | string | Name of the updated secret |
|
||||
| `arn` | string | ARN of the updated secret |
|
||||
| `versionId` | string | Version ID of the updated secret |
|
||||
|
||||
### `secrets_manager_delete_secret`
|
||||
|
||||
Delete a secret from AWS Secrets Manager
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `secretId` | string | Yes | The name or ARN of the secret to delete |
|
||||
| `recoveryWindowInDays` | number | No | Number of days before permanent deletion \(7-30, default 30\) |
|
||||
| `forceDelete` | boolean | No | If true, immediately delete without recovery window |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `name` | string | Name of the deleted secret |
|
||||
| `arn` | string | ARN of the deleted secret |
|
||||
| `deletionDate` | string | Scheduled deletion date |
|
||||
|
||||
|
||||
498
apps/docs/content/docs/en/tools/tailscale.mdx
Normal file
@@ -0,0 +1,498 @@
|
||||
---
|
||||
title: Tailscale
|
||||
description: Manage devices and network settings in your Tailscale tailnet
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="tailscale"
|
||||
color="#2E2D2D"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
## Overview
|
||||
|
||||
[Tailscale](https://tailscale.com) is a zero-config mesh VPN built on WireGuard that makes it easy to connect devices, services, and users across any network. The Tailscale block lets you automate network management tasks like device provisioning, access control, route management, and DNS configuration directly from your Sim workflows.
|
||||
|
||||
## Authentication
|
||||
|
||||
The Tailscale block uses API key authentication. To get an API key:
|
||||
|
||||
1. Go to the [Tailscale admin console](https://login.tailscale.com/admin/settings/keys)
|
||||
2. Navigate to **Settings > Keys**
|
||||
3. Click **Generate API key**
|
||||
4. Set an expiry (1-90 days) and copy the key (starts with `tskey-api-`)
|
||||
|
||||
You must have an **Owner**, **Admin**, **IT admin**, or **Network admin** role to generate API keys.
|
||||
|
||||
## Tailnet Identifier
|
||||
|
||||
Every operation requires a **tailnet** parameter. This is typically your organization's domain name (e.g., `example.com`). You can also use `"-"` to refer to your default tailnet.
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
- **Device inventory**: List and monitor all devices connected to your network
|
||||
- **Automated provisioning**: Create and manage auth keys to pre-authorize new devices
|
||||
- **Access control**: Authorize or deauthorize devices, manage device tags for ACL policies
|
||||
- **Route management**: View and enable subnet routes for devices acting as subnet routers
|
||||
- **DNS management**: Configure nameservers, MagicDNS, and search paths
|
||||
- **Key lifecycle**: Create, list, inspect, and revoke auth keys
|
||||
- **User auditing**: List all users in the tailnet and their roles
|
||||
- **Policy review**: Retrieve the current ACL policy for inspection or backup
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Interact with the Tailscale API to manage devices, DNS, ACLs, auth keys, users, and routes across your tailnet.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `tailscale_list_devices`
|
||||
|
||||
List all devices in the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `devices` | array | List of devices in the tailnet |
|
||||
| ↳ `id` | string | Device ID |
|
||||
| ↳ `name` | string | Device name |
|
||||
| ↳ `hostname` | string | Device hostname |
|
||||
| ↳ `user` | string | Associated user |
|
||||
| ↳ `os` | string | Operating system |
|
||||
| ↳ `clientVersion` | string | Tailscale client version |
|
||||
| ↳ `addresses` | array | Tailscale IP addresses |
|
||||
| ↳ `tags` | array | Device tags |
|
||||
| ↳ `authorized` | boolean | Whether the device is authorized |
|
||||
| ↳ `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections |
|
||||
| ↳ `lastSeen` | string | Last seen timestamp |
|
||||
| ↳ `created` | string | Creation timestamp |
|
||||
| `count` | number | Total number of devices |
|
||||
|
||||
### `tailscale_get_device`
|
||||
|
||||
Get details of a specific device by ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Device ID |
|
||||
| `name` | string | Device name |
|
||||
| `hostname` | string | Device hostname |
|
||||
| `user` | string | Associated user |
|
||||
| `os` | string | Operating system |
|
||||
| `clientVersion` | string | Tailscale client version |
|
||||
| `addresses` | array | Tailscale IP addresses |
|
||||
| `tags` | array | Device tags |
|
||||
| `authorized` | boolean | Whether the device is authorized |
|
||||
| `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections |
|
||||
| `lastSeen` | string | Last seen timestamp |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `isExternal` | boolean | Whether the device is external |
|
||||
| `updateAvailable` | boolean | Whether an update is available |
|
||||
| `machineKey` | string | Machine key |
|
||||
| `nodeKey` | string | Node key |
|
||||
|
||||
### `tailscale_delete_device`
|
||||
|
||||
Remove a device from the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the device was successfully deleted |
|
||||
| `deviceId` | string | ID of the deleted device |
|
||||
|
||||
### `tailscale_authorize_device`
|
||||
|
||||
Authorize or deauthorize a device on the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID to authorize |
|
||||
| `authorized` | boolean | Yes | Whether to authorize \(true\) or deauthorize \(false\) the device |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the operation succeeded |
|
||||
| `deviceId` | string | Device ID |
|
||||
| `authorized` | boolean | Authorization status after the operation |
|
||||
|
||||
### `tailscale_set_device_tags`
|
||||
|
||||
Set tags on a device in the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID |
|
||||
| `tags` | string | Yes | Comma-separated list of tags \(e.g., "tag:server,tag:production"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the tags were successfully set |
|
||||
| `deviceId` | string | Device ID |
|
||||
| `tags` | array | Tags set on the device |
|
||||
|
||||
### `tailscale_get_device_routes`
|
||||
|
||||
Get the subnet routes for a device
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `advertisedRoutes` | array | Subnet routes the device is advertising |
|
||||
| `enabledRoutes` | array | Subnet routes that are approved/enabled |
|
||||
|
||||
### `tailscale_set_device_routes`
|
||||
|
||||
Set the enabled subnet routes for a device
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID |
|
||||
| `routes` | string | Yes | Comma-separated list of subnet routes to enable \(e.g., "10.0.0.0/24,192.168.1.0/24"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `advertisedRoutes` | array | Subnet routes the device is advertising |
|
||||
| `enabledRoutes` | array | Subnet routes that are now enabled |
|
||||
|
||||
### `tailscale_update_device_key`
|
||||
|
||||
Enable or disable key expiry on a device
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `deviceId` | string | Yes | Device ID |
|
||||
| `keyExpiryDisabled` | boolean | Yes | Whether to disable key expiry \(true\) or enable it \(false\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the operation succeeded |
|
||||
| `deviceId` | string | Device ID |
|
||||
| `keyExpiryDisabled` | boolean | Whether key expiry is now disabled |
|
||||
|
||||
### `tailscale_list_dns_nameservers`
|
||||
|
||||
Get the DNS nameservers configured for the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `dns` | array | List of DNS nameserver addresses |
|
||||
| `magicDNS` | boolean | Whether MagicDNS is enabled |
|
||||
|
||||
### `tailscale_set_dns_nameservers`
|
||||
|
||||
Set the DNS nameservers for the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `dns` | string | Yes | Comma-separated list of DNS nameserver IP addresses \(e.g., "8.8.8.8,8.8.4.4"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `dns` | array | Updated list of DNS nameserver addresses |
|
||||
| `magicDNS` | boolean | Whether MagicDNS is enabled |
|
||||
|
||||
### `tailscale_get_dns_preferences`
|
||||
|
||||
Get the DNS preferences for the tailnet including MagicDNS status
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `magicDNS` | boolean | Whether MagicDNS is enabled |
|
||||
|
||||
### `tailscale_set_dns_preferences`
|
||||
|
||||
Set DNS preferences for the tailnet (enable/disable MagicDNS)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `magicDNS` | boolean | Yes | Whether to enable \(true\) or disable \(false\) MagicDNS |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `magicDNS` | boolean | Updated MagicDNS status |
|
||||
|
||||
### `tailscale_get_dns_searchpaths`
|
||||
|
||||
Get the DNS search paths configured for the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `searchPaths` | array | List of DNS search path domains |
|
||||
|
||||
### `tailscale_set_dns_searchpaths`
|
||||
|
||||
Set the DNS search paths for the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `searchPaths` | string | Yes | Comma-separated list of DNS search path domains \(e.g., "corp.example.com,internal.example.com"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `searchPaths` | array | Updated list of DNS search path domains |
|
||||
|
||||
### `tailscale_list_users`
|
||||
|
||||
List all users in the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | List of users in the tailnet |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `displayName` | string | Display name |
|
||||
| ↳ `loginName` | string | Login name / email |
|
||||
| ↳ `profilePicURL` | string | Profile picture URL |
|
||||
| ↳ `role` | string | User role \(owner, admin, member, etc.\) |
|
||||
| ↳ `status` | string | User status \(active, suspended, etc.\) |
|
||||
| ↳ `type` | string | User type \(member, shared, tagged\) |
|
||||
| ↳ `created` | string | Creation timestamp |
|
||||
| ↳ `lastSeen` | string | Last seen timestamp |
|
||||
| ↳ `deviceCount` | number | Number of devices owned by user |
|
||||
| `count` | number | Total number of users |
|
||||
|
||||
### `tailscale_create_auth_key`
|
||||
|
||||
Create a new auth key for the tailnet to pre-authorize devices
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `reusable` | boolean | No | Whether the key can be used more than once |
|
||||
| `ephemeral` | boolean | No | Whether devices authenticated with this key are ephemeral |
|
||||
| `preauthorized` | boolean | No | Whether devices are pre-authorized \(skip manual approval\) |
|
||||
| `tags` | string | No | Comma-separated list of tags for devices using this key \(e.g., "tag:server,tag:prod"\) |
|
||||
| `description` | string | No | Description for the auth key |
|
||||
| `expirySeconds` | number | No | Key expiry time in seconds \(default: 90 days\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Auth key ID |
|
||||
| `key` | string | The auth key value \(only shown once at creation\) |
|
||||
| `description` | string | Key description |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `expires` | string | Expiration timestamp |
|
||||
| `revoked` | string | Revocation timestamp \(empty if not revoked\) |
|
||||
| `capabilities` | object | Key capabilities |
|
||||
| ↳ `reusable` | boolean | Whether the key is reusable |
|
||||
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
|
||||
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
|
||||
| ↳ `tags` | array | Tags applied to devices using this key |
|
||||
|
||||
### `tailscale_list_auth_keys`
|
||||
|
||||
List all auth keys in the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `keys` | array | List of auth keys |
|
||||
| ↳ `id` | string | Auth key ID |
|
||||
| ↳ `description` | string | Key description |
|
||||
| ↳ `created` | string | Creation timestamp |
|
||||
| ↳ `expires` | string | Expiration timestamp |
|
||||
| ↳ `revoked` | string | Revocation timestamp |
|
||||
| ↳ `capabilities` | object | Key capabilities |
|
||||
| ↳ `reusable` | boolean | Whether the key is reusable |
|
||||
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
|
||||
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
|
||||
| ↳ `tags` | array | Tags applied to devices |
|
||||
| `count` | number | Total number of auth keys |
|
||||
|
||||
### `tailscale_get_auth_key`
|
||||
|
||||
Get details of a specific auth key
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `keyId` | string | Yes | Auth key ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Auth key ID |
|
||||
| `description` | string | Key description |
|
||||
| `created` | string | Creation timestamp |
|
||||
| `expires` | string | Expiration timestamp |
|
||||
| `revoked` | string | Revocation timestamp |
|
||||
| `capabilities` | object | Key capabilities |
|
||||
| ↳ `reusable` | boolean | Whether the key is reusable |
|
||||
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
|
||||
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
|
||||
| ↳ `tags` | array | Tags applied to devices using this key |
|
||||
|
||||
### `tailscale_delete_auth_key`
|
||||
|
||||
Revoke and delete an auth key
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
| `keyId` | string | Yes | Auth key ID to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the auth key was successfully deleted |
|
||||
| `keyId` | string | ID of the deleted auth key |
|
||||
|
||||
### `tailscale_get_acl`
|
||||
|
||||
Get the current ACL policy for the tailnet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Tailscale API key |
|
||||
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `acl` | string | ACL policy as JSON string |
|
||||
| `etag` | string | ETag for the current ACL version \(use with If-Match header for updates\) |
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ Detecta información de identificación personal utilizando Microsoft Presidio.
|
||||
**Casos de uso:**
|
||||
- Bloquear contenido que contiene información personal sensible
|
||||
- Enmascarar PII antes de registrar o almacenar datos
|
||||
- Cumplimiento de GDPR, HIPAA y otras regulaciones de privacidad
|
||||
- Cumplimiento de GDPR y otras regulaciones de privacidad
|
||||
- Sanear entradas de usuario antes del procesamiento
|
||||
|
||||
## Configuración
|
||||
|
||||
@@ -131,7 +131,7 @@ Détecte les informations personnelles identifiables à l'aide de Microsoft Pres
|
||||
**Cas d'utilisation :**
|
||||
- Bloquer le contenu contenant des informations personnelles sensibles
|
||||
- Masquer les PII avant de journaliser ou stocker des données
|
||||
- Conformité avec le RGPD, HIPAA et autres réglementations sur la confidentialité
|
||||
- Conformité avec le RGPD et autres réglementations sur la confidentialité
|
||||
- Assainir les entrées utilisateur avant traitement
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -131,7 +131,7 @@ Microsoft Presidioを使用して個人を特定できる情報を検出しま
|
||||
**ユースケース:**
|
||||
- 機密性の高い個人情報を含むコンテンツをブロック
|
||||
- データのログ記録や保存前にPIIをマスク
|
||||
- GDPR、HIPAA、その他のプライバシー規制への準拠
|
||||
- GDPR、その他のプライバシー規制への準拠
|
||||
- 処理前のユーザー入力のサニタイズ
|
||||
|
||||
## 設定
|
||||
|
||||
@@ -131,7 +131,7 @@ Guardrails 模块通过针对多种验证类型检查内容,验证并保护您
|
||||
**使用场景:**
|
||||
- 阻止包含敏感个人信息的内容
|
||||
- 在记录或存储数据之前屏蔽 PII
|
||||
- 符合 GDPR、HIPAA 和其他隐私法规
|
||||
- 符合 GDPR 和其他隐私法规
|
||||
- 在处理之前清理用户输入
|
||||
|
||||
## 配置
|
||||
|
||||
BIN
apps/docs/public/static/blocks/credential-loop.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
apps/docs/public/static/blocks/credential.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/docs/public/static/credentials/add-service-account.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
apps/docs/public/static/credentials/gcp-add-client-id.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
apps/docs/public/static/credentials/gcp-create-private-key.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -28,6 +28,15 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
|
||||
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models
|
||||
# VLLM_BASE_URL=http://localhost:8000 # Base URL for your self-hosted vLLM (OpenAI-compatible)
|
||||
# VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth
|
||||
# FIREWORKS_API_KEY= # Optional Fireworks AI API key for model listing
|
||||
# NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS=true # Set when using AWS default credential chain (IAM roles, ECS task roles, IRSA). Hides credential fields in Agent block UI.
|
||||
# AZURE_OPENAI_ENDPOINT= # Azure OpenAI endpoint (hides field in UI when set alongside NEXT_PUBLIC_AZURE_CONFIGURED)
|
||||
# AZURE_OPENAI_API_KEY= # Azure OpenAI API key
|
||||
# AZURE_OPENAI_API_VERSION= # Azure OpenAI API version
|
||||
# AZURE_ANTHROPIC_ENDPOINT= # Azure Anthropic endpoint (AI Foundry)
|
||||
# AZURE_ANTHROPIC_API_KEY= # Azure Anthropic API key
|
||||
# AZURE_ANTHROPIC_API_VERSION= # Azure Anthropic API version (e.g., 2023-06-01)
|
||||
# NEXT_PUBLIC_AZURE_CONFIGURED=true # Set when Azure credentials are pre-configured above. Hides endpoint/key/version fields in Agent block UI.
|
||||
|
||||
# Admin API (Optional - for self-hosted GitOps)
|
||||
# ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import.
|
||||
|
||||
33
apps/sim/AGENTS.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Sim App Scope
|
||||
|
||||
These rules apply to files under `apps/sim/` in addition to the repository root [AGENTS.md](/AGENTS.md).
|
||||
|
||||
## Architecture
|
||||
|
||||
- Follow the app structure already established under `app/`, `blocks/`, `components/`, `executor/`, `hooks/`, `lib/`, `providers/`, `stores/`, `tools/`, and `triggers/`.
|
||||
- Keep single responsibility for components, hooks, and stores.
|
||||
- Prefer composition over large mixed-responsibility modules.
|
||||
- Use `lib/` for app-wide helpers, feature-local `utils/` only when 2+ files share the helper, and inline single-use helpers.
|
||||
|
||||
## Imports And Types
|
||||
|
||||
- Always use absolute imports from `@/...`; do not add relative imports.
|
||||
- Use barrel exports only when a folder has 3+ exports; do not re-export through non-barrel files.
|
||||
- Use `import type` for type-only imports.
|
||||
- Do not use `any`; prefer precise types or `unknown` with guards.
|
||||
|
||||
## Components And Styling
|
||||
|
||||
- Use `'use client'` only when hooks or browser-only APIs are required.
|
||||
- Define a props interface for every component.
|
||||
- Extract constants with `as const` where appropriate.
|
||||
- Use Tailwind classes and `cn()` for conditional classes; avoid inline styles unless CSS variables are the intended mechanism.
|
||||
- Keep styling local to the component; do not modify global styles for feature work.
|
||||
|
||||
## Testing
|
||||
|
||||
- Use Vitest.
|
||||
- Prefer `@vitest-environment node` unless DOM APIs are required.
|
||||
- Use `vi.hoisted()` + `vi.mock()` + static imports; do not use `vi.resetModules()` + `vi.doMock()` + dynamic imports except for true module-scope singletons.
|
||||
- Do not use `vi.importActual()`.
|
||||
- Prefer mocks and factories from `@sim/testing`.
|
||||
@@ -14,8 +14,8 @@ export default function AuthLayoutClient({ children }: { children: React.ReactNo
|
||||
|
||||
return (
|
||||
<AuthBackground className='dark font-[430] font-season'>
|
||||
<main className='relative flex min-h-full flex-col text-[#ECECEC]'>
|
||||
<header className='shrink-0 bg-[#1C1C1C]'>
|
||||
<main className='relative flex min-h-full flex-col text-[var(--landing-text)]'>
|
||||
<header className='shrink-0 bg-[var(--landing-bg)]'>
|
||||
<Navbar logoOnly />
|
||||
</header>
|
||||
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
|
||||
|
||||
@@ -9,7 +9,7 @@ type AuthBackgroundProps = {
|
||||
export default function AuthBackground({ className, children }: AuthBackgroundProps) {
|
||||
return (
|
||||
<div className={cn('fixed inset-0 overflow-hidden', className)}>
|
||||
<div className='-z-50 pointer-events-none absolute inset-0 bg-[#1C1C1C]' />
|
||||
<div className='-z-50 pointer-events-none absolute inset-0 bg-[var(--landing-bg)]' />
|
||||
<AuthBackgroundSVG />
|
||||
<div className='relative z-20 h-full overflow-auto'>{children}</div>
|
||||
</div>
|
||||
|
||||
6
apps/sim/app/(auth)/components/auth-button-classes.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/** Shared className for primary auth/status CTA buttons on dark auth surfaces. */
|
||||
export const AUTH_PRIMARY_CTA_BASE =
|
||||
'inline-flex h-[32px] items-center justify-center gap-2 rounded-[5px] border border-[var(--auth-primary-btn-border)] bg-[var(--auth-primary-btn-bg)] px-2.5 font-[430] font-season text-[var(--auth-primary-btn-text)] text-sm transition-colors hover:border-[var(--auth-primary-btn-hover-border)] hover:bg-[var(--auth-primary-btn-hover-bg)] hover:text-[var(--auth-primary-btn-hover-text)] disabled:cursor-not-allowed disabled:opacity-50' as const
|
||||
|
||||
/** Full-width variant used for primary auth form submit buttons. */
|
||||
export const AUTH_SUBMIT_BTN = `${AUTH_PRIMARY_CTA_BASE} w-full` as const
|
||||
@@ -1,102 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { ArrowRight, ChevronRight, Loader2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
export interface BrandedButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
loading?: boolean
|
||||
loadingText?: string
|
||||
showArrow?: boolean
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Branded button for auth and status pages.
|
||||
* Default: white button matching the landing page "Get started" style.
|
||||
* Whitelabel: uses the brand's primary color as background with white text.
|
||||
*/
|
||||
export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
loading = false,
|
||||
loadingText,
|
||||
showArrow = true,
|
||||
fullWidth = true,
|
||||
className,
|
||||
disabled,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const brand = useBrandConfig()
|
||||
const hasCustomColor = brand.isWhitelabeled && Boolean(brand.theme?.primaryColor)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setIsHovered(true)
|
||||
onMouseEnter?.(e)
|
||||
}
|
||||
|
||||
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setIsHovered(false)
|
||||
onMouseLeave?.(e)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
{...props}
|
||||
disabled={disabled || loading}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={cn(
|
||||
'group inline-flex h-[32px] items-center justify-center gap-[8px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px] transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
!hasCustomColor &&
|
||||
'border-[#FFFFFF] bg-[#FFFFFF] text-black hover:border-[#E0E0E0] hover:bg-[#E0E0E0]',
|
||||
fullWidth && 'w-full',
|
||||
className
|
||||
)}
|
||||
style={
|
||||
hasCustomColor
|
||||
? {
|
||||
backgroundColor: isHovered
|
||||
? (brand.theme?.primaryHoverColor ?? brand.theme?.primaryColor)
|
||||
: brand.theme?.primaryColor,
|
||||
borderColor: isHovered
|
||||
? (brand.theme?.primaryHoverColor ?? brand.theme?.primaryColor)
|
||||
: brand.theme?.primaryColor,
|
||||
color: '#FFFFFF',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
{loadingText ? `${loadingText}...` : children}
|
||||
</span>
|
||||
) : showArrow ? (
|
||||
<span className='flex items-center gap-1'>
|
||||
{children}
|
||||
<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>
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BrandedButton.displayName = 'BrandedButton'
|
||||
@@ -4,23 +4,18 @@ import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/emcn'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
|
||||
interface SSOLoginButtonProps {
|
||||
callbackURL?: string
|
||||
className?: string
|
||||
// Visual variant for button styling and placement contexts
|
||||
// - 'primary' matches the main auth action button style
|
||||
// - 'outline' matches social provider buttons
|
||||
variant?: 'primary' | 'outline'
|
||||
// Optional class used when variant is primary to match brand/gradient
|
||||
primaryClassName?: string
|
||||
}
|
||||
|
||||
export function SSOLoginButton({
|
||||
callbackURL,
|
||||
className,
|
||||
variant = 'outline',
|
||||
primaryClassName,
|
||||
}: SSOLoginButtonProps) {
|
||||
const router = useRouter()
|
||||
|
||||
@@ -33,11 +28,6 @@ export function SSOLoginButton({
|
||||
router.push(ssoUrl)
|
||||
}
|
||||
|
||||
const primaryBtnClasses = cn(
|
||||
primaryClassName || 'branded-button-gradient',
|
||||
'flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200'
|
||||
)
|
||||
|
||||
const outlineBtnClasses = cn('w-full rounded-[10px]')
|
||||
|
||||
return (
|
||||
@@ -45,7 +35,7 @@ export function SSOLoginButton({
|
||||
type='button'
|
||||
onClick={handleSSOClick}
|
||||
variant={variant === 'outline' ? 'outline' : undefined}
|
||||
className={cn(variant === 'outline' ? outlineBtnClasses : primaryBtnClasses, className)}
|
||||
className={cn(variant === 'outline' ? outlineBtnClasses : AUTH_SUBMIT_BTN, className)}
|
||||
>
|
||||
Sign in with SSO
|
||||
</Button>
|
||||
|
||||
@@ -18,18 +18,18 @@ export function StatusPageLayout({
|
||||
}: StatusPageLayoutProps) {
|
||||
return (
|
||||
<AuthBackground className='dark font-[430] font-season'>
|
||||
<main className='relative flex min-h-full flex-col text-[#ECECEC]'>
|
||||
<header className='shrink-0 bg-[#1C1C1C]'>
|
||||
<main className='relative flex min-h-full flex-col text-[var(--landing-text)]'>
|
||||
<header className='shrink-0 bg-[var(--landing-bg)]'>
|
||||
<Navbar logoOnly />
|
||||
</header>
|
||||
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
|
||||
<div className='w-full max-w-lg px-4'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
{title}
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -11,12 +11,12 @@ export function SupportFooter({ position = 'fixed' }: SupportFooterProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed ${position}`}
|
||||
className={`right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[var(--landing-text-muted)] text-small leading-relaxed ${position}`}
|
||||
>
|
||||
Need help?{' '}
|
||||
<a
|
||||
href={`mailto:${brandConfig.supportEmail}`}
|
||||
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
|
||||
className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline'
|
||||
>
|
||||
Contact support
|
||||
</a>
|
||||
|
||||
@@ -4,21 +4,21 @@ export default function LoginLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Skeleton className='h-[38px] w-[80px] rounded-[4px]' />
|
||||
<div className='mt-[32px] w-full space-y-[8px]'>
|
||||
<div className='mt-8 w-full space-y-2'>
|
||||
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<div className='mt-[16px] w-full space-y-[8px]'>
|
||||
<div className='mt-4 w-full space-y-2'>
|
||||
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-[24px] h-[1px] w-full rounded-[1px]' />
|
||||
<div className='mt-[24px] flex w-full gap-[12px]'>
|
||||
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-6 h-[1px] w-full rounded-[1px]' />
|
||||
<div className='mt-6 flex w-full gap-3'>
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[14px] w-[200px] rounded-[4px]' />
|
||||
<Skeleton className='mt-6 h-[14px] w-[200px] rounded-[4px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import {
|
||||
@@ -21,10 +20,9 @@ import { validateCallbackUrl } from '@/lib/core/security/input-validation'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||
|
||||
const logger = createLogger('LoginForm')
|
||||
|
||||
@@ -88,10 +86,6 @@ export default function LoginPage({
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
const [showValidationError, setShowValidationError] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
const turnstileRef = useRef<TurnstileInstance>(null)
|
||||
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
|
||||
const buttonClass = useBrandedButtonClass()
|
||||
|
||||
const callbackUrlParam = searchParams?.get('callbackUrl')
|
||||
const isValidCallbackUrl = callbackUrlParam ? validateCallbackUrl(callbackUrlParam) : false
|
||||
const invalidCallbackRef = useRef(false)
|
||||
@@ -169,20 +163,6 @@ export default function LoginPage({
|
||||
const safeCallbackUrl = callbackUrl
|
||||
let errorHandled = false
|
||||
|
||||
// Execute Turnstile challenge on submit and get a fresh token
|
||||
let token: string | undefined
|
||||
if (turnstileSiteKey && turnstileRef.current) {
|
||||
try {
|
||||
turnstileRef.current.reset()
|
||||
turnstileRef.current.execute()
|
||||
token = await turnstileRef.current.getResponsePromise(15_000)
|
||||
} catch {
|
||||
setFormError('Captcha verification failed. Please try again.')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setFormError(null)
|
||||
const result = await client.signIn.email(
|
||||
{
|
||||
@@ -191,12 +171,7 @@ export default function LoginPage({
|
||||
callbackURL: safeCallbackUrl,
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
...(token ? { 'x-captcha-response': token } : {}),
|
||||
},
|
||||
},
|
||||
onError: (ctx) => {
|
||||
onError: (ctx: any) => {
|
||||
logger.error('Login error:', ctx.error)
|
||||
|
||||
if (ctx.error.code?.includes('EMAIL_NOT_VERIFIED')) {
|
||||
@@ -364,10 +339,10 @@ export default function LoginPage({
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
Sign in
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
|
||||
Enter your details
|
||||
</p>
|
||||
</div>
|
||||
@@ -375,11 +350,7 @@ export default function LoginPage({
|
||||
{/* SSO Login Button (primary top-only when it is the only method) */}
|
||||
{showTopSSO && (
|
||||
<div className='mt-8'>
|
||||
<SSOLoginButton
|
||||
callbackURL={callbackUrl}
|
||||
variant='primary'
|
||||
primaryClassName={buttonClass}
|
||||
/>
|
||||
<SSOLoginButton callbackURL={callbackUrl} variant='primary' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -421,7 +392,7 @@ export default function LoginPage({
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setForgotPasswordOpen(true)}
|
||||
className='font-medium text-[#999] text-xs transition hover:text-[#ECECEC]'
|
||||
className='font-medium text-[var(--landing-text-muted)] text-xs transition hover:text-[var(--landing-text)]'
|
||||
>
|
||||
Forgot password?
|
||||
</button>
|
||||
@@ -448,7 +419,7 @@ export default function LoginPage({
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--landing-text-muted)] transition hover:text-[var(--landing-text)]'
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
@@ -464,16 +435,6 @@ export default function LoginPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{turnstileSiteKey && (
|
||||
<div className='absolute'>
|
||||
<Turnstile
|
||||
ref={turnstileRef}
|
||||
siteKey={turnstileSiteKey}
|
||||
options={{ size: 'invisible', execution: 'execute' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{resetSuccessMessage && (
|
||||
<div className='text-[#4CAF50] text-xs'>
|
||||
<p>{resetSuccessMessage}</p>
|
||||
@@ -486,14 +447,16 @@ export default function LoginPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
loadingText='Signing in'
|
||||
>
|
||||
Sign in
|
||||
</BrandedButton>
|
||||
<button type='submit' disabled={isLoading} className={AUTH_SUBMIT_BTN}>
|
||||
{isLoading ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Signing in...
|
||||
</span>
|
||||
) : (
|
||||
'Sign in'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -501,10 +464,12 @@ export default function LoginPage({
|
||||
{showDivider && (
|
||||
<div className='relative my-6 font-light'>
|
||||
<div className='absolute inset-0 flex items-center'>
|
||||
<div className='w-full border-[#2A2A2A] border-t' />
|
||||
<div className='w-full border-[var(--landing-bg-elevated)] border-t' />
|
||||
</div>
|
||||
<div className='relative flex justify-center text-sm'>
|
||||
<span className='bg-[#1C1C1C] px-4 font-[340] text-[#999]'>Or continue with</span>
|
||||
<span className='bg-[var(--landing-bg)] px-4 font-[340] text-[var(--landing-text-muted)]'>
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -518,11 +483,7 @@ export default function LoginPage({
|
||||
callbackURL={callbackUrl}
|
||||
>
|
||||
{ssoEnabled && !hasOnlySSO && (
|
||||
<SSOLoginButton
|
||||
callbackURL={callbackUrl}
|
||||
variant='outline'
|
||||
primaryClassName={buttonClass}
|
||||
/>
|
||||
<SSOLoginButton callbackURL={callbackUrl} variant='outline' />
|
||||
)}
|
||||
</SocialLoginButtons>
|
||||
</div>
|
||||
@@ -534,20 +495,20 @@ export default function LoginPage({
|
||||
<span className='font-normal'>Don't have an account? </span>
|
||||
<Link
|
||||
href={isInviteFlow ? `/signup?invite_flow=true&callbackUrl=${callbackUrl}` : '/signup'}
|
||||
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
|
||||
className='font-medium text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed sm:px-8 md:px-[44px]'>
|
||||
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] text-[var(--landing-text-muted)] leading-relaxed sm:px-8 md:px-11'>
|
||||
By signing in, you agree to our{' '}
|
||||
<Link
|
||||
href='/terms'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
|
||||
className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline'
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
@@ -556,7 +517,7 @@ export default function LoginPage({
|
||||
href='/privacy'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
|
||||
className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline'
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
@@ -601,14 +562,16 @@ export default function LoginPage({
|
||||
<p>{resetStatus.message}</p>
|
||||
</div>
|
||||
)}
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isSubmittingReset}
|
||||
loading={isSubmittingReset}
|
||||
loadingText='Sending'
|
||||
>
|
||||
Send Reset Link
|
||||
</BrandedButton>
|
||||
<button type='submit' disabled={isSubmittingReset} className={AUTH_SUBMIT_BTN}>
|
||||
{isSubmittingReset ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Sending...
|
||||
</span>
|
||||
) : (
|
||||
'Send Reset Link'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
|
||||
@@ -3,16 +3,16 @@ import { Skeleton } from '@/components/emcn'
|
||||
export default function OAuthConsentLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<div className='flex items-center gap-[16px]'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
|
||||
<Skeleton className='h-[20px] w-[20px] rounded-[4px]' />
|
||||
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[38px] w-[220px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[8px] h-[14px] w-[280px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[24px] h-[56px] w-full rounded-[8px]' />
|
||||
<Skeleton className='mt-[16px] h-[120px] w-full rounded-[8px]' />
|
||||
<div className='mt-[24px] flex w-full max-w-[410px] gap-[12px]'>
|
||||
<Skeleton className='mt-6 h-[38px] w-[220px] rounded-[4px]' />
|
||||
<Skeleton className='mt-2 h-[14px] w-[280px] rounded-[4px]' />
|
||||
<Skeleton className='mt-6 h-[56px] w-full rounded-[8px]' />
|
||||
<Skeleton className='mt-4 h-[120px] w-full rounded-[8px]' />
|
||||
<div className='mt-6 flex w-full max-w-[410px] gap-3'>
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ArrowLeftRight } from 'lucide-react'
|
||||
import { ArrowLeftRight, Loader2 } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Button } from '@/components/emcn'
|
||||
import { signOut, useSession } from '@/lib/auth/auth-client'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
|
||||
const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
openid: 'Verify your identity',
|
||||
@@ -127,10 +127,10 @@ export default function OAuthConsentPage() {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
Authorize Application
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
|
||||
Loading application details...
|
||||
</p>
|
||||
</div>
|
||||
@@ -142,15 +142,17 @@ export default function OAuthConsentPage() {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
Authorization Error
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
<div className='mt-8 w-full max-w-[410px] space-y-3'>
|
||||
<BrandedButton onClick={() => router.push('/')}>Return to Home</BrandedButton>
|
||||
<button onClick={() => router.push('/')} className={AUTH_SUBMIT_BTN}>
|
||||
Return to Home
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -170,11 +172,11 @@ export default function OAuthConsentPage() {
|
||||
className='rounded-[10px]'
|
||||
/>
|
||||
) : (
|
||||
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-[#2A2A2A] font-medium text-[#999] text-[18px]'>
|
||||
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-[var(--landing-bg-elevated)] font-medium text-[var(--landing-text-muted)] text-lg'>
|
||||
{(clientName ?? '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<ArrowLeftRight className='h-5 w-5 text-[#999]' />
|
||||
<ArrowLeftRight className='h-5 w-5 text-[var(--landing-text-muted)]' />
|
||||
<Image
|
||||
src='/new/logo/colorized-bg.svg'
|
||||
alt='Sim'
|
||||
@@ -185,17 +187,17 @@ export default function OAuthConsentPage() {
|
||||
</div>
|
||||
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
Authorize Application
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<span className='font-medium text-[#ECECEC]'>{clientName}</span> is requesting access to
|
||||
your account
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
|
||||
<span className='font-medium text-[var(--landing-text)]'>{clientName}</span> is requesting
|
||||
access to your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{session?.user && (
|
||||
<div className='mt-5 flex items-center gap-3 rounded-lg border border-[#2A2A2A] px-4 py-3'>
|
||||
<div className='mt-5 flex items-center gap-3 rounded-lg border border-[var(--landing-bg-elevated)] px-4 py-3'>
|
||||
{session.user.image ? (
|
||||
<Image
|
||||
src={session.user.image}
|
||||
@@ -206,20 +208,22 @@ export default function OAuthConsentPage() {
|
||||
unoptimized
|
||||
/>
|
||||
) : (
|
||||
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-[#2A2A2A] font-medium text-[#999] text-[13px]'>
|
||||
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-[var(--landing-bg-elevated)] font-medium text-[var(--landing-text-muted)] text-small'>
|
||||
{(session.user.name ?? session.user.email ?? '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className='min-w-0'>
|
||||
{session.user.name && (
|
||||
<p className='truncate font-medium text-[14px]'>{session.user.name}</p>
|
||||
<p className='truncate font-medium text-sm'>{session.user.name}</p>
|
||||
)}
|
||||
<p className='truncate text-[#999] text-[13px]'>{session.user.email}</p>
|
||||
<p className='truncate text-[var(--landing-text-muted)] text-small'>
|
||||
{session.user.email}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSwitchAccount}
|
||||
className='ml-auto text-[#999] text-[13px] underline-offset-2 transition-colors hover:text-[#ECECEC] hover:underline'
|
||||
className='ml-auto text-[var(--landing-text-muted)] text-small underline-offset-2 transition-colors hover:text-[var(--landing-text)] hover:underline'
|
||||
>
|
||||
Switch
|
||||
</button>
|
||||
@@ -228,11 +232,14 @@ export default function OAuthConsentPage() {
|
||||
|
||||
{scopes.length > 0 && (
|
||||
<div className='mt-5 w-full max-w-[410px]'>
|
||||
<div className='rounded-lg border p-4'>
|
||||
<p className='mb-3 font-medium text-[14px]'>This will allow the application to:</p>
|
||||
<div className='rounded-lg border border-[var(--landing-bg-elevated)] p-4'>
|
||||
<p className='mb-3 font-medium text-sm'>This will allow the application to:</p>
|
||||
<ul className='space-y-2'>
|
||||
{scopes.map((s) => (
|
||||
<li key={s} className='flex items-start gap-2 font-normal text-[#999] text-[13px]'>
|
||||
<li
|
||||
key={s}
|
||||
className='flex items-start gap-2 font-normal text-[var(--landing-text-muted)] text-small'
|
||||
>
|
||||
<span className='mt-0.5 text-green-500'>✓</span>
|
||||
<span>{SCOPE_DESCRIPTIONS[s] ?? s}</span>
|
||||
</li>
|
||||
@@ -252,15 +259,20 @@ export default function OAuthConsentPage() {
|
||||
>
|
||||
Deny
|
||||
</Button>
|
||||
<BrandedButton
|
||||
fullWidth
|
||||
showArrow={false}
|
||||
loading={submitting}
|
||||
loadingText='Authorizing'
|
||||
<button
|
||||
onClick={() => handleConsent(true)}
|
||||
disabled={submitting}
|
||||
className={AUTH_SUBMIT_BTN}
|
||||
>
|
||||
Allow
|
||||
</BrandedButton>
|
||||
{submitting ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Authorizing...
|
||||
</span>
|
||||
) : (
|
||||
'Allow'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,13 +4,13 @@ export default function ResetPasswordLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Skeleton className='h-[38px] w-[160px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[12px] h-[14px] w-[280px] rounded-[4px]' />
|
||||
<div className='mt-[32px] w-full space-y-[8px]'>
|
||||
<Skeleton className='mt-3 h-[14px] w-[280px] rounded-[4px]' />
|
||||
<div className='mt-8 w-full space-y-2'>
|
||||
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-[24px] h-[14px] w-[120px] rounded-[4px]' />
|
||||
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-6 h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ function ResetPasswordContent() {
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
Reset your password
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
|
||||
Enter a new password for your account
|
||||
</p>
|
||||
</div>
|
||||
@@ -87,10 +87,10 @@ function ResetPasswordContent() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='pt-6 text-center font-light text-[14px]'>
|
||||
<div className='pt-6 text-center font-light text-sm'>
|
||||
<Link
|
||||
href='/login'
|
||||
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
|
||||
className='font-medium text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
|
||||
>
|
||||
Back to login
|
||||
</Link>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import { Input, Label } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
|
||||
interface RequestResetFormProps {
|
||||
email: string
|
||||
@@ -46,7 +46,7 @@ export function RequestResetForm({
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
/>
|
||||
<p className='text-[#999] text-sm'>
|
||||
<p className='text-[var(--landing-text-muted)] text-sm'>
|
||||
We'll send a password reset link to this email address.
|
||||
</p>
|
||||
</div>
|
||||
@@ -54,21 +54,26 @@ export function RequestResetForm({
|
||||
{/* Status message display */}
|
||||
{statusType && statusMessage && (
|
||||
<div
|
||||
className={cn('text-xs', statusType === 'success' ? 'text-[#4CAF50]' : 'text-red-400')}
|
||||
className={cn(
|
||||
'text-xs',
|
||||
statusType === 'success' ? 'text-[var(--success)]' : 'text-red-400'
|
||||
)}
|
||||
>
|
||||
<p>{statusMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
loadingText='Sending'
|
||||
>
|
||||
Send Reset Link
|
||||
</BrandedButton>
|
||||
<button type='submit' disabled={isSubmitting} className={AUTH_SUBMIT_BTN}>
|
||||
{isSubmitting ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Sending...
|
||||
</span>
|
||||
) : (
|
||||
'Send Reset Link'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -162,7 +167,7 @@ export function SetNewPasswordForm({
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--landing-text-muted)] transition hover:text-[var(--landing-text)]'
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
@@ -190,7 +195,7 @@ export function SetNewPasswordForm({
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--landing-text-muted)] transition hover:text-[var(--landing-text)]'
|
||||
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
@@ -208,7 +213,7 @@ export function SetNewPasswordForm({
|
||||
<div
|
||||
className={cn(
|
||||
'mt-1 space-y-1 text-xs',
|
||||
statusType === 'success' ? 'text-[#4CAF50]' : 'text-red-400'
|
||||
statusType === 'success' ? 'text-[var(--success)]' : 'text-red-400'
|
||||
)}
|
||||
>
|
||||
<p>{statusMessage}</p>
|
||||
@@ -216,14 +221,16 @@ export function SetNewPasswordForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isSubmitting || !token}
|
||||
loading={isSubmitting}
|
||||
loadingText='Resetting'
|
||||
>
|
||||
Reset Password
|
||||
</BrandedButton>
|
||||
<button type='submit' disabled={isSubmitting || !token} className={AUTH_SUBMIT_BTN}>
|
||||
{isSubmitting ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Resetting...
|
||||
</span>
|
||||
) : (
|
||||
'Reset Password'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,25 +4,25 @@ export default function SignupLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Skeleton className='h-[38px] w-[100px] rounded-[4px]' />
|
||||
<div className='mt-[32px] w-full space-y-[8px]'>
|
||||
<div className='mt-8 w-full space-y-2'>
|
||||
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<div className='mt-[16px] w-full space-y-[8px]'>
|
||||
<div className='mt-4 w-full space-y-2'>
|
||||
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<div className='mt-[16px] w-full space-y-[8px]'>
|
||||
<div className='mt-4 w-full space-y-2'>
|
||||
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-[24px] h-[1px] w-full rounded-[1px]' />
|
||||
<div className='mt-[24px] flex w-full gap-[12px]'>
|
||||
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-6 h-[1px] w-full rounded-[1px]' />
|
||||
<div className='mt-6 flex w-full gap-3'>
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[14px] w-[220px] rounded-[4px]' />
|
||||
<Skeleton className='mt-6 h-[14px] w-[220px] rounded-[4px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, useMemo, useRef, useState } from 'react'
|
||||
import { Suspense, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { Eye, EyeOff, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { usePostHog } from 'posthog-js/react'
|
||||
import { Input, Label } from '@/components/emcn'
|
||||
import { client, useSession } from '@/lib/auth/auth-client'
|
||||
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
|
||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||
|
||||
const logger = createLogger('SignupForm')
|
||||
|
||||
@@ -82,7 +83,12 @@ function SignupFormContent({
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { refetch: refetchSession } = useSession()
|
||||
const posthog = usePostHog()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
captureEvent(posthog, 'signup_page_viewed', {})
|
||||
}, [posthog])
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
@@ -93,9 +99,9 @@ function SignupFormContent({
|
||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
const turnstileRef = useRef<TurnstileInstance>(null)
|
||||
const captchaResolveRef = useRef<((token: string) => void) | null>(null)
|
||||
const captchaRejectRef = useRef<((reason: Error) => void) | null>(null)
|
||||
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
|
||||
const buttonClass = useBrandedButtonClass()
|
||||
|
||||
const redirectUrl = useMemo(
|
||||
() => searchParams.get('redirect') || searchParams.get('callbackUrl') || '',
|
||||
[searchParams]
|
||||
@@ -249,17 +255,30 @@ function SignupFormContent({
|
||||
|
||||
const sanitizedName = trimmedName
|
||||
|
||||
// Execute Turnstile challenge on submit and get a fresh token
|
||||
let token: string | undefined
|
||||
if (turnstileSiteKey && turnstileRef.current) {
|
||||
const widget = turnstileRef.current
|
||||
if (turnstileSiteKey && widget) {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
turnstileRef.current.reset()
|
||||
turnstileRef.current.execute()
|
||||
token = await turnstileRef.current.getResponsePromise(15_000)
|
||||
widget.reset()
|
||||
token = await Promise.race([
|
||||
new Promise<string>((resolve, reject) => {
|
||||
captchaResolveRef.current = resolve
|
||||
captchaRejectRef.current = reject
|
||||
widget.execute()
|
||||
}),
|
||||
new Promise<string>((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error('Captcha timed out')), 15_000)
|
||||
}),
|
||||
])
|
||||
} catch {
|
||||
setFormError('Captcha verification failed. Please try again.')
|
||||
setIsLoading(false)
|
||||
return
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
captchaResolveRef.current = null
|
||||
captchaRejectRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,10 +367,10 @@ function SignupFormContent({
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
Create an account
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
|
||||
Create an account or log in
|
||||
</p>
|
||||
</div>
|
||||
@@ -365,126 +384,162 @@ function SignupFormContent({
|
||||
return hasOnlySSO
|
||||
})() && (
|
||||
<div className='mt-8'>
|
||||
<SSOLoginButton
|
||||
callbackURL={redirectUrl || '/workspace'}
|
||||
variant='primary'
|
||||
primaryClassName={buttonClass}
|
||||
/>
|
||||
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='primary' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email/Password Form - show unless explicitly disabled */}
|
||||
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
|
||||
<form onSubmit={onSubmit} className='mt-8 space-y-8'>
|
||||
<form onSubmit={onSubmit} className='mt-8 space-y-10'>
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label htmlFor='name'>Full name</Label>
|
||||
</div>
|
||||
<Input
|
||||
id='name'
|
||||
name='name'
|
||||
placeholder='Enter your name'
|
||||
type='text'
|
||||
autoCapitalize='words'
|
||||
autoComplete='name'
|
||||
title='Name can only contain letters, spaces, hyphens, and apostrophes'
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
className={cn(
|
||||
showNameValidationError &&
|
||||
nameErrors.length > 0 &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
{showNameValidationError && nameErrors.length > 0 && (
|
||||
<div className='mt-1 space-y-1 text-red-400 text-xs'>
|
||||
{nameErrors.map((error, index) => (
|
||||
<p key={index}>{error}</p>
|
||||
))}
|
||||
<div className='relative'>
|
||||
<Input
|
||||
id='name'
|
||||
name='name'
|
||||
placeholder='Enter your name'
|
||||
type='text'
|
||||
autoCapitalize='words'
|
||||
autoComplete='name'
|
||||
title='Name can only contain letters, spaces, hyphens, and apostrophes'
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
className={cn(
|
||||
showNameValidationError &&
|
||||
nameErrors.length > 0 &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 left-0 z-10 grid transition-[grid-template-rows] duration-200 ease-out',
|
||||
showNameValidationError && nameErrors.length > 0
|
||||
? 'grid-rows-[1fr]'
|
||||
: 'grid-rows-[0fr]'
|
||||
)}
|
||||
aria-live={showNameValidationError && nameErrors.length > 0 ? 'polite' : 'off'}
|
||||
>
|
||||
<div className='overflow-hidden'>
|
||||
<div className='mt-1 space-y-1 text-red-400 text-xs'>
|
||||
{nameErrors.map((error, index) => (
|
||||
<p key={index}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label htmlFor='email'>Email</Label>
|
||||
</div>
|
||||
<Input
|
||||
id='email'
|
||||
name='email'
|
||||
placeholder='Enter your email'
|
||||
autoCapitalize='none'
|
||||
autoComplete='email'
|
||||
autoCorrect='off'
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
className={cn(
|
||||
(emailError || (showEmailValidationError && emailErrors.length > 0)) &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
{showEmailValidationError && emailErrors.length > 0 && (
|
||||
<div className='mt-1 space-y-1 text-red-400 text-xs'>
|
||||
{emailErrors.map((error, index) => (
|
||||
<p key={index}>{error}</p>
|
||||
))}
|
||||
<div className='relative'>
|
||||
<Input
|
||||
id='email'
|
||||
name='email'
|
||||
placeholder='Enter your email'
|
||||
autoCapitalize='none'
|
||||
autoComplete='email'
|
||||
autoCorrect='off'
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
className={cn(
|
||||
(emailError || (showEmailValidationError && emailErrors.length > 0)) &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 left-0 z-10 grid transition-[grid-template-rows] duration-200 ease-out',
|
||||
(showEmailValidationError && emailErrors.length > 0) ||
|
||||
(emailError && !showEmailValidationError)
|
||||
? 'grid-rows-[1fr]'
|
||||
: 'grid-rows-[0fr]'
|
||||
)}
|
||||
aria-live={
|
||||
(showEmailValidationError && emailErrors.length > 0) ||
|
||||
(emailError && !showEmailValidationError)
|
||||
? 'polite'
|
||||
: 'off'
|
||||
}
|
||||
>
|
||||
<div className='overflow-hidden'>
|
||||
<div className='mt-1 space-y-1 text-red-400 text-xs'>
|
||||
{showEmailValidationError && emailErrors.length > 0 ? (
|
||||
emailErrors.map((error, index) => <p key={index}>{error}</p>)
|
||||
) : emailError && !showEmailValidationError ? (
|
||||
<p>{emailError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{emailError && !showEmailValidationError && (
|
||||
<div className='mt-1 text-red-400 text-xs'>
|
||||
<p>{emailError}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label htmlFor='password'>Password</Label>
|
||||
</div>
|
||||
<div className='relative'>
|
||||
<Input
|
||||
id='password'
|
||||
name='password'
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoCapitalize='none'
|
||||
autoComplete='new-password'
|
||||
placeholder='Enter your password'
|
||||
autoCorrect='off'
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
className={cn(
|
||||
'pr-10',
|
||||
showValidationError &&
|
||||
passwordErrors.length > 0 &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
{showValidationError && passwordErrors.length > 0 && (
|
||||
<div className='mt-1 space-y-1 text-red-400 text-xs'>
|
||||
{passwordErrors.map((error, index) => (
|
||||
<p key={index}>{error}</p>
|
||||
))}
|
||||
<div className='relative'>
|
||||
<Input
|
||||
id='password'
|
||||
name='password'
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoCapitalize='none'
|
||||
autoComplete='new-password'
|
||||
placeholder='Enter your password'
|
||||
autoCorrect='off'
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
className={cn(
|
||||
'pr-10',
|
||||
showValidationError &&
|
||||
passwordErrors.length > 0 &&
|
||||
'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--landing-text-muted)] transition hover:text-[var(--landing-text)]'
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 left-0 z-10 grid transition-[grid-template-rows] duration-200 ease-out',
|
||||
showValidationError && passwordErrors.length > 0
|
||||
? 'grid-rows-[1fr]'
|
||||
: 'grid-rows-[0fr]'
|
||||
)}
|
||||
aria-live={showValidationError && passwordErrors.length > 0 ? 'polite' : 'off'}
|
||||
>
|
||||
<div className='overflow-hidden'>
|
||||
<div className='mt-1 space-y-1 text-red-400 text-xs'>
|
||||
{passwordErrors.map((error, index) => (
|
||||
<p key={index}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{turnstileSiteKey && (
|
||||
<div className='absolute'>
|
||||
<Turnstile
|
||||
ref={turnstileRef}
|
||||
siteKey={turnstileSiteKey}
|
||||
options={{ size: 'invisible', execution: 'execute' }}
|
||||
/>
|
||||
</div>
|
||||
<Turnstile
|
||||
ref={turnstileRef}
|
||||
siteKey={turnstileSiteKey}
|
||||
onSuccess={(token) => captchaResolveRef.current?.(token)}
|
||||
onError={() => captchaRejectRef.current?.(new Error('Captcha verification failed'))}
|
||||
onExpire={() => captchaRejectRef.current?.(new Error('Captcha token expired'))}
|
||||
options={{ execution: 'execute' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{formError && (
|
||||
@@ -493,14 +548,16 @@ function SignupFormContent({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
loadingText='Creating account'
|
||||
>
|
||||
Create account
|
||||
</BrandedButton>
|
||||
<button type='submit' disabled={isLoading} className={cn('!mt-6', AUTH_SUBMIT_BTN)}>
|
||||
{isLoading ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Creating account...
|
||||
</span>
|
||||
) : (
|
||||
'Create account'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -516,10 +573,12 @@ function SignupFormContent({
|
||||
})() && (
|
||||
<div className='relative my-6 font-light'>
|
||||
<div className='absolute inset-0 flex items-center'>
|
||||
<div className='w-full border-[#2A2A2A] border-t' />
|
||||
<div className='w-full border-[var(--landing-bg-elevated)] border-t' />
|
||||
</div>
|
||||
<div className='relative flex justify-center text-sm'>
|
||||
<span className='bg-[#1C1C1C] px-4 font-[340] text-[#999]'>Or continue with</span>
|
||||
<span className='bg-[var(--landing-bg)] px-4 font-[340] text-[var(--landing-text-muted)]'>
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -544,33 +603,29 @@ function SignupFormContent({
|
||||
isProduction={isProduction}
|
||||
>
|
||||
{isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) && (
|
||||
<SSOLoginButton
|
||||
callbackURL={redirectUrl || '/workspace'}
|
||||
variant='outline'
|
||||
primaryClassName={buttonClass}
|
||||
/>
|
||||
<SSOLoginButton callbackURL={redirectUrl || '/workspace'} variant='outline' />
|
||||
)}
|
||||
</SocialLoginButtons>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='pt-6 text-center font-light text-[14px]'>
|
||||
<div className='pt-6 text-center font-light text-sm'>
|
||||
<span className='font-normal'>Already have an account? </span>
|
||||
<Link
|
||||
href={isInviteFlow ? `/login?invite_flow=true&callbackUrl=${redirectUrl}` : '/login'}
|
||||
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
|
||||
className='font-medium text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed sm:px-8 md:px-[44px]'>
|
||||
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[var(--landing-text-muted)] text-small leading-relaxed sm:px-8 md:px-11'>
|
||||
By creating an account, you agree to our{' '}
|
||||
<Link
|
||||
href='/terms'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
|
||||
className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline'
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
@@ -579,7 +634,7 @@ function SignupFormContent({
|
||||
href='/privacy'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
|
||||
className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline'
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
|
||||
@@ -4,13 +4,13 @@ export default function SSOLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Skeleton className='h-[38px] w-[120px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[12px] h-[14px] w-[260px] rounded-[4px]' />
|
||||
<div className='mt-[32px] w-full space-y-[8px]'>
|
||||
<Skeleton className='mt-3 h-[14px] w-[260px] rounded-[4px]' />
|
||||
<div className='mt-8 w-full space-y-2'>
|
||||
<Skeleton className='h-[14px] w-[80px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-[24px] h-[14px] w-[120px] rounded-[4px]' />
|
||||
<Skeleton className='mt-6 h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-6 h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ export default function VerifyLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Skeleton className='h-[38px] w-[180px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[12px] h-[14px] w-[300px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[4px] h-[14px] w-[240px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[32px] h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-3 h-[14px] w-[300px] rounded-[4px]' />
|
||||
<Skeleton className='mt-1 h-[14px] w-[240px] rounded-[4px]' />
|
||||
<Skeleton className='mt-8 h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, useEffect, useState } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { BrandedButton } from '@/app/(auth)/components/branded-button'
|
||||
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
|
||||
import { useVerification } from '@/app/(auth)/verify/use-verification'
|
||||
|
||||
interface VerifyContentProps {
|
||||
@@ -59,10 +60,10 @@ function VerificationForm({
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
|
||||
{isVerified ? 'Email Verified!' : 'Verify Your Email'}
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[18px] leading-[125%] tracking-[0.02em]'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
|
||||
{isVerified
|
||||
? 'Your email has been verified. Redirecting to dashboard...'
|
||||
: !isEmailVerificationEnabled
|
||||
@@ -78,7 +79,7 @@ function VerificationForm({
|
||||
{!isVerified && isEmailVerificationEnabled && (
|
||||
<div className='mt-8 space-y-8'>
|
||||
<div className='space-y-6'>
|
||||
<p className='text-center text-[#999] text-sm'>
|
||||
<p className='text-center text-[var(--landing-text-muted)] text-sm'>
|
||||
Enter the 6-digit code to verify your account.
|
||||
{hasEmailService ? " If you don't see it in your inbox, check your spam folder." : ''}
|
||||
</p>
|
||||
@@ -110,27 +111,33 @@ function VerificationForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BrandedButton
|
||||
<button
|
||||
onClick={verifyCode}
|
||||
disabled={!isOtpComplete || isLoading}
|
||||
loading={isLoading}
|
||||
loadingText='Verifying'
|
||||
showArrow={false}
|
||||
className={AUTH_SUBMIT_BTN}
|
||||
>
|
||||
Verify Email
|
||||
</BrandedButton>
|
||||
{isLoading ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
Verifying...
|
||||
</span>
|
||||
) : (
|
||||
'Verify Email'
|
||||
)}
|
||||
</button>
|
||||
|
||||
{hasEmailService && (
|
||||
<div className='text-center'>
|
||||
<p className='text-[#999] text-sm'>
|
||||
<p className='text-[var(--landing-text-muted)] text-sm'>
|
||||
Didn't receive a code?{' '}
|
||||
{countdown > 0 ? (
|
||||
<span>
|
||||
Resend in <span className='font-medium text-[#ECECEC]'>{countdown}s</span>
|
||||
Resend in{' '}
|
||||
<span className='font-medium text-[var(--landing-text)]'>{countdown}s</span>
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
|
||||
className='font-medium text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
|
||||
onClick={handleResend}
|
||||
disabled={isLoading || isResendDisabled}
|
||||
>
|
||||
@@ -141,7 +148,7 @@ function VerificationForm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='text-center font-light text-[14px]'>
|
||||
<div className='text-center font-light text-sm'>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -151,7 +158,7 @@ function VerificationForm({
|
||||
}
|
||||
router.push('/signup')
|
||||
}}
|
||||
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
|
||||
className='font-medium text-[var(--landing-text)] underline-offset-4 transition hover:text-white hover:underline'
|
||||
>
|
||||
Back to signup
|
||||
</button>
|
||||
@@ -166,8 +173,8 @@ function VerificationFormFallback() {
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<div className='animate-pulse'>
|
||||
<div className='mx-auto mb-4 h-8 w-48 rounded bg-[#2A2A2A]' />
|
||||
<div className='mx-auto h-4 w-64 rounded bg-[#2A2A2A]' />
|
||||
<div className='mx-auto mb-4 h-8 w-48 rounded bg-[var(--surface-4)]' />
|
||||
<div className='mx-auto h-4 w-64 rounded bg-[var(--surface-4)]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: cols * rows }, (_, i) => (
|
||||
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[#2A2A2A]' />
|
||||
<div key={i} className='h-[1.5px] w-[1.5px] rounded-full bg-[var(--landing-bg-elevated)]' />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -89,7 +89,7 @@ function VikhyathCursor() {
|
||||
<div className='absolute top-0 left-[56.02px]'>
|
||||
<CursorArrow fill='#2ABBF8' />
|
||||
</div>
|
||||
<div className='-left-[4px] absolute top-[18px] flex items-center rounded bg-[#2ABBF8] px-[5px] py-[3px] font-[420] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
|
||||
<div className='-left-[4px] absolute top-4.5 flex items-center rounded bg-[#2ABBF8] px-[5px] py-[3px] font-[420] font-season text-[var(--landing-text-dark)] text-sm leading-[100%] tracking-[-0.02em]'>
|
||||
Vikhyath
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,7 +113,7 @@ function AlexaCursor() {
|
||||
<div className='absolute top-0 left-0'>
|
||||
<CursorArrow fill='#FFCC02' />
|
||||
</div>
|
||||
<div className='absolute top-[16px] left-[23px] flex items-center rounded bg-[#FFCC02] px-[5px] py-[3px] font-[420] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
|
||||
<div className='absolute top-4 left-[23px] flex items-center rounded bg-[#FFCC02] px-[5px] py-[3px] font-[420] font-season text-[var(--landing-text-dark)] text-sm leading-[100%] tracking-[-0.02em]'>
|
||||
Alexa
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +143,7 @@ function YouCursor({ x, y, visible }: YouCursorProps) {
|
||||
<svg width='23.15' height='21.1' viewBox='0 0 17.5 16.4' fill='none'>
|
||||
<path d={CURSOR_ARROW_MIRRORED_PATH} fill='#33C482' />
|
||||
</svg>
|
||||
<div className='absolute top-[16px] left-[23px] flex items-center rounded bg-[#33C482] px-[5px] py-[3px] font-[420] font-season text-[#202020] text-[14px] leading-[100%] tracking-[-0.02em]'>
|
||||
<div className='absolute top-4 left-[23px] flex items-center rounded bg-[var(--brand-accent)] px-[5px] py-[3px] font-[420] font-season text-[var(--landing-text-dark)] text-sm leading-[100%] tracking-[-0.02em]'>
|
||||
You
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,7 +212,7 @@ export default function Collaboration() {
|
||||
ref={sectionRef}
|
||||
id='collaboration'
|
||||
aria-labelledby='collaboration-heading'
|
||||
className='bg-[#1C1C1C]'
|
||||
className='bg-[var(--landing-bg)]'
|
||||
style={{ cursor: isHovering ? 'none' : 'auto' }}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
@@ -222,7 +222,7 @@ export default function Collaboration() {
|
||||
<style dangerouslySetInnerHTML={{ __html: CURSOR_KEYFRAMES }} />
|
||||
|
||||
<DotGrid
|
||||
className='overflow-hidden border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
className='overflow-hidden border-[var(--landing-bg-elevated)] border-y bg-[var(--landing-bg)] p-1.5'
|
||||
cols={120}
|
||||
rows={1}
|
||||
gap={6}
|
||||
@@ -230,33 +230,33 @@ export default function Collaboration() {
|
||||
|
||||
<div className='relative overflow-hidden'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-[auto_1fr]'>
|
||||
<div className='flex flex-col items-start gap-3 px-4 pt-[60px] pb-8 sm:gap-4 sm:px-8 md:gap-[20px] md:px-[80px] md:pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-3 px-4 pt-[60px] pb-8 sm:gap-4 sm:px-8 md:gap-5 md:px-20 md:pt-[100px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
dot
|
||||
className='bg-[#33C482]/10 font-season text-[#33C482] uppercase tracking-[0.02em]'
|
||||
className='bg-[color-mix(in_srgb,var(--brand-accent)_10%,transparent)] font-season text-[var(--brand-accent)] uppercase tracking-[0.02em]'
|
||||
>
|
||||
Teams
|
||||
</Badge>
|
||||
|
||||
<h2
|
||||
id='collaboration-heading'
|
||||
className='font-[430] font-season text-[32px] text-white leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
|
||||
className='text-balance font-[430] font-season text-[32px] text-white leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
|
||||
>
|
||||
Realtime
|
||||
<br />
|
||||
collaboration
|
||||
</h2>
|
||||
|
||||
<p className='font-[430] font-season text-[#F6F6F0]/50 text-[15px] leading-[150%] tracking-[0.02em] md:text-[18px]'>
|
||||
<p className='font-[430] font-season text-[#F6F6F0]/50 text-base leading-[150%] tracking-[0.02em] md:text-lg'>
|
||||
Grab your team. Build agents together <br className='hidden md:block' />
|
||||
in real-time inside your workspace.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href='/signup'
|
||||
className='group/cta mt-[12px] inline-flex h-[32px] cursor-none items-center gap-[6px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
className='group/cta mt-3 inline-flex h-[32px] cursor-none items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Build together
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
@@ -288,7 +288,6 @@ export default function Collaboration() {
|
||||
width={876}
|
||||
height={480}
|
||||
className='h-full w-auto object-left md:min-w-[100vw]'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className='hidden lg:block'>
|
||||
@@ -306,16 +305,16 @@ export default function Collaboration() {
|
||||
href='/blog/multiplayer'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='relative mx-4 mb-6 flex cursor-none items-center gap-[14px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] px-[12px] py-[10px] transition-colors hover:border-[#3d3d3d] hover:bg-[#232323] sm:mx-8 md:absolute md:bottom-10 md:left-[80px] md:z-20 md:mx-0 md:mb-0'
|
||||
className='relative mx-4 mb-6 flex cursor-none items-center gap-3.5 rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] px-3 py-2.5 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-card)] sm:mx-8 md:absolute md:bottom-10 md:left-20 md:z-20 md:mx-0 md:mb-0'
|
||||
>
|
||||
<div className='relative h-7 w-11 shrink-0'>
|
||||
<Image src='/landing/multiplayer-cursors.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-[2px]'>
|
||||
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[12px] uppercase leading-[100%] tracking-[0.08em]'>
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<span className='font-[430] font-season text-[#F6F6F0]/50 text-caption uppercase leading-[100%] tracking-[0.08em]'>
|
||||
Blog
|
||||
</span>
|
||||
<span className='font-[430] font-season text-[#F6F6F0] text-[14px] leading-[125%] tracking-[0.02em]'>
|
||||
<span className='font-[430] font-season text-[#F6F6F0] text-sm leading-[125%] tracking-[0.02em]'>
|
||||
How we built realtime collaboration
|
||||
</span>
|
||||
</div>
|
||||
@@ -323,7 +322,7 @@ export default function Collaboration() {
|
||||
</div>
|
||||
|
||||
<DotGrid
|
||||
className='overflow-hidden border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
|
||||
className='overflow-hidden border-[var(--landing-bg-elevated)] border-y bg-[var(--landing-bg)] p-1.5'
|
||||
cols={120}
|
||||
rows={1}
|
||||
gap={6}
|
||||
|
||||
93
apps/sim/app/(home)/components/demo-request/consts.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import freeEmailDomains from 'free-email-domains'
|
||||
import { z } from 'zod'
|
||||
import { NO_EMAIL_HEADER_CONTROL_CHARS_REGEX } from '@/lib/messaging/email/utils'
|
||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||
|
||||
const FREE_EMAIL_DOMAINS = new Set(freeEmailDomains)
|
||||
|
||||
export const DEMO_REQUEST_REGION_VALUES = [
|
||||
'north_america',
|
||||
'europe',
|
||||
'asia_pacific',
|
||||
'latin_america',
|
||||
'middle_east_africa',
|
||||
'other',
|
||||
] as const
|
||||
|
||||
export const DEMO_REQUEST_COMPANY_SIZE_VALUES = [
|
||||
'1_10',
|
||||
'11_50',
|
||||
'51_200',
|
||||
'201_500',
|
||||
'501_1000',
|
||||
'1001_10000',
|
||||
'10000_plus',
|
||||
] as const
|
||||
|
||||
export const DEMO_REQUEST_REGION_OPTIONS = [
|
||||
{ value: 'north_america', label: 'North America' },
|
||||
{ value: 'europe', label: 'Europe' },
|
||||
{ value: 'asia_pacific', label: 'Asia Pacific' },
|
||||
{ value: 'latin_america', label: 'Latin America' },
|
||||
{ value: 'middle_east_africa', label: 'Middle East & Africa' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
] as const
|
||||
|
||||
export const DEMO_REQUEST_COMPANY_SIZE_OPTIONS = [
|
||||
{ value: '1_10', label: '1–10' },
|
||||
{ value: '11_50', label: '11–50' },
|
||||
{ value: '51_200', label: '51–200' },
|
||||
{ value: '201_500', label: '201–500' },
|
||||
{ value: '501_1000', label: '501–1,000' },
|
||||
{ value: '1001_10000', label: '1,001–10,000' },
|
||||
{ value: '10000_plus', label: '10,000+' },
|
||||
] as const
|
||||
|
||||
export const demoRequestSchema = z.object({
|
||||
firstName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'First name is required')
|
||||
.max(100)
|
||||
.regex(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX, 'Invalid characters'),
|
||||
lastName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Last name is required')
|
||||
.max(100)
|
||||
.regex(NO_EMAIL_HEADER_CONTROL_CHARS_REGEX, 'Invalid characters'),
|
||||
companyEmail: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Company email is required')
|
||||
.max(320)
|
||||
.transform((value) => value.toLowerCase())
|
||||
.refine((value) => quickValidateEmail(value).isValid, 'Enter a valid work email')
|
||||
.refine((value) => {
|
||||
const domain = value.split('@')[1]
|
||||
return domain ? !FREE_EMAIL_DOMAINS.has(domain) : true
|
||||
}, 'Please use your work email address'),
|
||||
phoneNumber: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(50, 'Phone number must be 50 characters or less')
|
||||
.optional()
|
||||
.transform((value) => (value && value.length > 0 ? value : undefined)),
|
||||
region: z.enum(DEMO_REQUEST_REGION_VALUES, {
|
||||
errorMap: () => ({ message: 'Please select a region' }),
|
||||
}),
|
||||
companySize: z.enum(DEMO_REQUEST_COMPANY_SIZE_VALUES, {
|
||||
errorMap: () => ({ message: 'Please select company size' }),
|
||||
}),
|
||||
details: z.string().trim().min(1, 'Details are required').max(2000),
|
||||
})
|
||||
|
||||
export type DemoRequestPayload = z.infer<typeof demoRequestSchema>
|
||||
|
||||
export function getDemoRequestRegionLabel(value: DemoRequestPayload['region']): string {
|
||||
return DEMO_REQUEST_REGION_OPTIONS.find((option) => option.value === value)?.label ?? value
|
||||
}
|
||||
|
||||
export function getDemoRequestCompanySizeLabel(value: DemoRequestPayload['companySize']): string {
|
||||
return DEMO_REQUEST_COMPANY_SIZE_OPTIONS.find((option) => option.value === value)?.label ?? value
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
FormField,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTrigger,
|
||||
Textarea,
|
||||
} from '@/components/emcn'
|
||||
import { Check } from '@/components/emcn/icons'
|
||||
import {
|
||||
DEMO_REQUEST_COMPANY_SIZE_OPTIONS,
|
||||
DEMO_REQUEST_REGION_OPTIONS,
|
||||
type DemoRequestPayload,
|
||||
demoRequestSchema,
|
||||
} from '@/app/(home)/components/demo-request/consts'
|
||||
|
||||
interface DemoRequestModalProps {
|
||||
children: React.ReactNode
|
||||
theme?: 'dark' | 'light'
|
||||
}
|
||||
|
||||
type DemoRequestField = keyof DemoRequestPayload
|
||||
type DemoRequestErrors = Partial<Record<DemoRequestField, string>>
|
||||
|
||||
interface DemoRequestFormState {
|
||||
firstName: string
|
||||
lastName: string
|
||||
companyEmail: string
|
||||
phoneNumber: string
|
||||
region: DemoRequestPayload['region'] | ''
|
||||
companySize: DemoRequestPayload['companySize'] | ''
|
||||
details: string
|
||||
}
|
||||
|
||||
const SUBMIT_SUCCESS_MESSAGE = "We'll be in touch soon!"
|
||||
const COMBOBOX_REGIONS = [...DEMO_REQUEST_REGION_OPTIONS]
|
||||
const COMBOBOX_COMPANY_SIZES = [...DEMO_REQUEST_COMPANY_SIZE_OPTIONS]
|
||||
|
||||
const INITIAL_FORM_STATE: DemoRequestFormState = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
companyEmail: '',
|
||||
phoneNumber: '',
|
||||
region: '',
|
||||
companySize: '',
|
||||
details: '',
|
||||
}
|
||||
|
||||
export function DemoRequestModal({ children, theme = 'dark' }: DemoRequestModalProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [form, setForm] = useState<DemoRequestFormState>(INITIAL_FORM_STATE)
|
||||
const [errors, setErrors] = useState<DemoRequestErrors>({})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false)
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setForm(INITIAL_FORM_STATE)
|
||||
setErrors({})
|
||||
setIsSubmitting(false)
|
||||
setSubmitError(null)
|
||||
setSubmitSuccess(false)
|
||||
}, [])
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
setOpen(nextOpen)
|
||||
resetForm()
|
||||
},
|
||||
[resetForm]
|
||||
)
|
||||
|
||||
const updateField = useCallback(
|
||||
<TField extends keyof DemoRequestFormState>(
|
||||
field: TField,
|
||||
value: DemoRequestFormState[TField]
|
||||
) => {
|
||||
setForm((prev) => ({ ...prev, [field]: value }))
|
||||
setErrors((prev) => {
|
||||
if (!prev[field]) {
|
||||
return prev
|
||||
}
|
||||
|
||||
const nextErrors = { ...prev }
|
||||
delete nextErrors[field]
|
||||
return nextErrors
|
||||
})
|
||||
setSubmitError(null)
|
||||
setSubmitSuccess(false)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
setSubmitError(null)
|
||||
setSubmitSuccess(false)
|
||||
|
||||
const parsed = demoRequestSchema.safeParse({
|
||||
...form,
|
||||
phoneNumber: form.phoneNumber || undefined,
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
const fieldErrors = parsed.error.flatten().fieldErrors
|
||||
setErrors({
|
||||
firstName: fieldErrors.firstName?.[0],
|
||||
lastName: fieldErrors.lastName?.[0],
|
||||
companyEmail: fieldErrors.companyEmail?.[0],
|
||||
phoneNumber: fieldErrors.phoneNumber?.[0],
|
||||
region: fieldErrors.region?.[0],
|
||||
companySize: fieldErrors.companySize?.[0],
|
||||
details: fieldErrors.details?.[0],
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/demo-requests', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(parsed.data),
|
||||
})
|
||||
|
||||
const result = (await response.json().catch(() => null)) as {
|
||||
error?: string
|
||||
message?: string
|
||||
} | null
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result?.error || 'Failed to submit demo request')
|
||||
}
|
||||
|
||||
setSubmitSuccess(true)
|
||||
} catch (error) {
|
||||
setSubmitError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to submit demo request. Please try again.'
|
||||
)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
},
|
||||
[form, resetForm]
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal open={open} onOpenChange={handleOpenChange}>
|
||||
<ModalTrigger asChild>{children}</ModalTrigger>
|
||||
<ModalContent size='lg' className={theme === 'dark' ? 'dark' : undefined}>
|
||||
<ModalHeader>
|
||||
<span className={submitSuccess ? 'sr-only' : undefined}>
|
||||
{submitSuccess ? 'Demo request submitted' : 'Nearly there!'}
|
||||
</span>
|
||||
</ModalHeader>
|
||||
<div className='relative flex-1'>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
aria-hidden={submitSuccess}
|
||||
className={
|
||||
submitSuccess
|
||||
? 'pointer-events-none invisible flex h-full flex-col'
|
||||
: 'flex h-full flex-col'
|
||||
}
|
||||
>
|
||||
<ModalBody>
|
||||
<div className='space-y-4'>
|
||||
<div className='grid gap-4 sm:grid-cols-2'>
|
||||
<FormField htmlFor='firstName' label='First name' error={errors.firstName}>
|
||||
<Input
|
||||
id='firstName'
|
||||
value={form.firstName}
|
||||
onChange={(event) => updateField('firstName', event.target.value)}
|
||||
placeholder='First'
|
||||
/>
|
||||
</FormField>
|
||||
<FormField htmlFor='lastName' label='Last name' error={errors.lastName}>
|
||||
<Input
|
||||
id='lastName'
|
||||
value={form.lastName}
|
||||
onChange={(event) => updateField('lastName', event.target.value)}
|
||||
placeholder='Last'
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField htmlFor='companyEmail' label='Company email' error={errors.companyEmail}>
|
||||
<Input
|
||||
id='companyEmail'
|
||||
type='email'
|
||||
value={form.companyEmail}
|
||||
onChange={(event) => updateField('companyEmail', event.target.value)}
|
||||
placeholder='Your work email'
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
htmlFor='phoneNumber'
|
||||
label='Phone number'
|
||||
optional
|
||||
error={errors.phoneNumber}
|
||||
>
|
||||
<Input
|
||||
id='phoneNumber'
|
||||
type='tel'
|
||||
value={form.phoneNumber}
|
||||
onChange={(event) => updateField('phoneNumber', event.target.value)}
|
||||
placeholder='Your phone number'
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className='grid gap-4 sm:grid-cols-2'>
|
||||
<FormField htmlFor='region' label='Region' error={errors.region}>
|
||||
<Combobox
|
||||
options={COMBOBOX_REGIONS}
|
||||
value={form.region}
|
||||
selectedValue={form.region}
|
||||
onChange={(value) =>
|
||||
updateField('region', value as DemoRequestPayload['region'])
|
||||
}
|
||||
placeholder='Select'
|
||||
editable={false}
|
||||
filterOptions={false}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField htmlFor='companySize' label='Company size' error={errors.companySize}>
|
||||
<Combobox
|
||||
options={COMBOBOX_COMPANY_SIZES}
|
||||
value={form.companySize}
|
||||
selectedValue={form.companySize}
|
||||
onChange={(value) =>
|
||||
updateField('companySize', value as DemoRequestPayload['companySize'])
|
||||
}
|
||||
placeholder='Select'
|
||||
editable={false}
|
||||
filterOptions={false}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField htmlFor='details' label='Details' error={errors.details}>
|
||||
<Textarea
|
||||
id='details'
|
||||
value={form.details}
|
||||
onChange={(event) => updateField('details', event.target.value)}
|
||||
placeholder='Tell us about your needs and questions'
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className='flex-col items-stretch gap-3'>
|
||||
{submitError && <p className='text-[13px] text-[var(--text-error)]'>{submitError}</p>}
|
||||
<Button type='submit' variant='primary' disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
|
||||
{submitSuccess ? (
|
||||
<div className='absolute inset-0 flex items-center justify-center px-8 pb-10 sm:px-12 sm:pb-14'>
|
||||
<div className='flex max-w-md flex-col items-center justify-center text-center'>
|
||||
<div className='flex h-20 w-20 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--bg-subtle)] text-[var(--text-primary)]'>
|
||||
<Check className='h-10 w-10' />
|
||||
</div>
|
||||
<h2 className='mt-8 font-medium text-[34px] text-[var(--text-primary)] leading-[1.1] tracking-[-0.03em]'>
|
||||
{SUBMIT_SUCCESS_MESSAGE}
|
||||
</h2>
|
||||
<p className='mt-4 text-[17px] text-[var(--text-secondary)] leading-7'>
|
||||
Our team will be in touch soon. If you have any questions, please email us at{' '}
|
||||
<a
|
||||
href='mailto:enterprise@sim.ai'
|
||||
className='text-[var(--text-primary)] underline underline-offset-2'
|
||||
>
|
||||
enterprise@sim.ai
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -81,6 +81,56 @@ function ProviderPreviewIcon({ providerId }: { providerId?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
interface FeatureToggleItemProps {
|
||||
feature: PermissionFeature
|
||||
enabled: boolean
|
||||
color: string
|
||||
isInView: boolean
|
||||
delay: number
|
||||
textClassName: string
|
||||
transition: Record<string, unknown>
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
function FeatureToggleItem({
|
||||
feature,
|
||||
enabled,
|
||||
color,
|
||||
isInView,
|
||||
delay,
|
||||
textClassName,
|
||||
transition,
|
||||
onToggle,
|
||||
}: FeatureToggleItemProps) {
|
||||
return (
|
||||
<motion.div
|
||||
key={feature.key}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-label={`Toggle ${feature.name}`}
|
||||
aria-pressed={enabled}
|
||||
className='flex cursor-pointer items-center gap-2 rounded-[4px] py-0.5'
|
||||
initial={{ opacity: 0, x: -6 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{ ...transition, delay }}
|
||||
onClick={onToggle}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onToggle()
|
||||
}
|
||||
}}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<CheckboxIcon checked={enabled} color={color} />
|
||||
<ProviderPreviewIcon providerId={feature.providerId} />
|
||||
<span className={textClassName} style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}>
|
||||
{feature.name}
|
||||
</span>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AccessControlPanel() {
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { once: true, margin: '-40px' })
|
||||
@@ -97,39 +147,25 @@ export function AccessControlPanel() {
|
||||
|
||||
return (
|
||||
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/55 text-[10px] uppercase leading-none tracking-[0.08em]'>
|
||||
{category.label}
|
||||
</span>
|
||||
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
|
||||
{category.features.map((feature, featIdx) => {
|
||||
const enabled = accessState[feature.key]
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={feature.key}
|
||||
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
|
||||
initial={{ opacity: 0, x: -6 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{
|
||||
delay: 0.05 + (offsetBefore + featIdx) * 0.04,
|
||||
duration: 0.3,
|
||||
}}
|
||||
onClick={() =>
|
||||
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
|
||||
}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<CheckboxIcon checked={enabled} color={category.color} />
|
||||
<ProviderPreviewIcon providerId={feature.providerId} />
|
||||
<span
|
||||
className='truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
|
||||
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
|
||||
>
|
||||
{feature.name}
|
||||
</span>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
<div className='mt-2 grid grid-cols-2 gap-x-4 gap-y-2'>
|
||||
{category.features.map((feature, featIdx) => (
|
||||
<FeatureToggleItem
|
||||
key={feature.key}
|
||||
feature={feature}
|
||||
enabled={accessState[feature.key]}
|
||||
color={category.color}
|
||||
isInView={isInView}
|
||||
delay={0.05 + (offsetBefore + featIdx) * 0.04}
|
||||
textClassName='truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
|
||||
transition={{ duration: 0.3 }}
|
||||
onToggle={() =>
|
||||
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -140,12 +176,11 @@ export function AccessControlPanel() {
|
||||
<div className='hidden lg:block'>
|
||||
{PERMISSION_CATEGORIES.map((category, catIdx) => (
|
||||
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/55 text-[10px] uppercase leading-none tracking-[0.08em]'>
|
||||
{category.label}
|
||||
</span>
|
||||
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
|
||||
<div className='mt-2 grid grid-cols-2 gap-x-4 gap-y-2'>
|
||||
{category.features.map((feature, featIdx) => {
|
||||
const enabled = accessState[feature.key]
|
||||
const currentIndex =
|
||||
PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
|
||||
(sum, c) => sum + c.features.length,
|
||||
@@ -153,30 +188,19 @@ export function AccessControlPanel() {
|
||||
) + featIdx
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<FeatureToggleItem
|
||||
key={feature.key}
|
||||
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
|
||||
initial={{ opacity: 0, x: -6 }}
|
||||
animate={isInView ? { opacity: 1, x: 0 } : {}}
|
||||
transition={{
|
||||
delay: 0.1 + currentIndex * 0.04,
|
||||
duration: 0.3,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
}}
|
||||
onClick={() =>
|
||||
feature={feature}
|
||||
enabled={accessState[feature.key]}
|
||||
color={category.color}
|
||||
isInView={isInView}
|
||||
delay={0.1 + currentIndex * 0.04}
|
||||
textClassName='truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
|
||||
transition={{ duration: 0.3, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
onToggle={() =>
|
||||
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
|
||||
}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<CheckboxIcon checked={enabled} color={category.color} />
|
||||
<ProviderPreviewIcon providerId={feature.providerId} />
|
||||
<span
|
||||
className='truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
|
||||
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
|
||||
>
|
||||
{feature.name}
|
||||
</span>
|
||||
</motion.div>
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -125,7 +125,7 @@ function AuditRow({ entry, index }: AuditRowProps) {
|
||||
const timeAgo = formatTimeAgo(entry.insertedAt)
|
||||
|
||||
return (
|
||||
<div className='group relative overflow-hidden border-[#2A2A2A] border-b bg-[#1C1C1C] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
|
||||
<div className='group relative overflow-hidden border-[var(--landing-border)] border-b bg-[var(--landing-bg)] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
|
||||
{/* Left accent bar -- brightness encodes recency */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
@@ -146,14 +146,14 @@ function AuditRow({ entry, index }: AuditRowProps) {
|
||||
</div>
|
||||
|
||||
{/* Time */}
|
||||
<span className='w-[56px] shrink-0 font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em]'>
|
||||
<span className='w-[56px] shrink-0 font-[430] font-season text-[#F6F6F6]/55 text-[11px] leading-none tracking-[0.02em]'>
|
||||
{timeAgo}
|
||||
</span>
|
||||
|
||||
<span className='min-w-0 truncate font-[430] font-season text-[12px] leading-none tracking-[0.02em]'>
|
||||
<span className='text-[#F6F6F6]/80'>{entry.actor}</span>
|
||||
<span className='hidden sm:inline'>
|
||||
<span className='text-[#F6F6F6]/40'> · </span>
|
||||
<span className='text-[#F6F6F6]/60'> · </span>
|
||||
<span className='text-[#F6F6F6]/55'>{entry.description}</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -166,14 +166,14 @@ export function AuditLogPreview() {
|
||||
const counterRef = useRef(ENTRY_TEMPLATES.length)
|
||||
const templateIndexRef = useRef(6 % ENTRY_TEMPLATES.length)
|
||||
|
||||
const now = Date.now()
|
||||
const [entries, setEntries] = useState<LogEntry[]>(() =>
|
||||
ENTRY_TEMPLATES.slice(0, 6).map((t, i) => ({
|
||||
const [entries, setEntries] = useState<LogEntry[]>(() => {
|
||||
const now = Date.now()
|
||||
return ENTRY_TEMPLATES.slice(0, 6).map((t, i) => ({
|
||||
...t,
|
||||
id: i,
|
||||
insertedAt: now - INITIAL_OFFSETS_MS[i],
|
||||
}))
|
||||
)
|
||||
})
|
||||
const [, tick] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -208,10 +208,9 @@ export function AuditLogPreview() {
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
layout: {
|
||||
type: 'spring',
|
||||
stiffness: 380,
|
||||
damping: 38,
|
||||
mass: 0.8,
|
||||
type: 'tween',
|
||||
duration: 0.32,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
},
|
||||
y: { duration: 0.32, ease: [0.25, 0.46, 0.45, 0.94] },
|
||||
opacity: { duration: 0.25 },
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* SEO:
|
||||
* - `<section id="enterprise" aria-labelledby="enterprise-heading">`.
|
||||
* - `<h2 id="enterprise-heading">` for the section title.
|
||||
* - Compliance certs (SOC 2, HIPAA) as visible `<strong>` text.
|
||||
* - Compliance cert (SOC 2) as visible `<strong>` text.
|
||||
* - Enterprise CTA links to contact form via `<a>` with `rel="noopener noreferrer"`.
|
||||
*
|
||||
* GEO:
|
||||
* - Entity-rich: "Sim is SOC 2 and HIPAA compliant" — not "We are compliant."
|
||||
* - Entity-rich: "Sim is SOC 2 compliant" — not "We are compliant."
|
||||
* - `<ul>` checklist of features (SSO, RBAC, audit logs, SLA, on-premise deployment)
|
||||
* as an atomic answer block for "What enterprise features does Sim offer?".
|
||||
*/
|
||||
@@ -18,16 +18,31 @@ import Link from 'next/link'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
import { Lock } from '@/components/emcn/icons'
|
||||
import { GithubIcon } from '@/components/icons'
|
||||
import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-request-modal'
|
||||
import { AccessControlPanel } from '@/app/(home)/components/enterprise/components/access-control-panel'
|
||||
import { AuditLogPreview } from '@/app/(home)/components/enterprise/components/audit-log-preview'
|
||||
|
||||
const MARQUEE_KEYFRAMES = `
|
||||
@keyframes marquee {
|
||||
const ENTERPRISE_FEATURE_MARQUEE_STYLES = `
|
||||
@keyframes enterprise-feature-marquee {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-25%); }
|
||||
}
|
||||
.enterprise-feature-marquee-track {
|
||||
animation: enterprise-feature-marquee 30s linear infinite;
|
||||
}
|
||||
.enterprise-feature-marquee:hover .enterprise-feature-marquee-track {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.enterprise-feature-marquee-tag {
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@keyframes marquee { 0%, 100% { transform: none; } }
|
||||
.enterprise-feature-marquee-track {
|
||||
animation: none;
|
||||
}
|
||||
.enterprise-feature-marquee-tag {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -50,13 +65,13 @@ const FEATURE_TAGS = [
|
||||
|
||||
function TrustStrip() {
|
||||
return (
|
||||
<div className='mx-6 mt-4 grid grid-cols-1 overflow-hidden rounded-[8px] border border-[#2A2A2A] sm:grid-cols-3 md:mx-8'>
|
||||
{/* SOC 2 + HIPAA combined */}
|
||||
<div className='mx-6 mt-4 grid grid-cols-1 overflow-hidden rounded-lg border border-[var(--landing-bg-elevated)] sm:grid-cols-3 md:mx-8'>
|
||||
{/* SOC 2 */}
|
||||
<Link
|
||||
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group flex items-center gap-3 border-[#2A2A2A] border-b px-4 py-[14px] transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
|
||||
className='group flex items-center gap-3 border-[var(--landing-bg-elevated)] border-b px-4 py-3.5 transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
|
||||
>
|
||||
<Image
|
||||
src='/footer/soc2.png'
|
||||
@@ -64,13 +79,14 @@ function TrustStrip() {
|
||||
width={22}
|
||||
height={22}
|
||||
className='shrink-0 object-contain'
|
||||
unoptimized
|
||||
/>
|
||||
<div className='flex flex-col gap-[3px]'>
|
||||
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
|
||||
SOC 2 & HIPAA
|
||||
<strong className='font-[430] font-season text-small text-white leading-none'>
|
||||
SOC 2
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em] transition-colors group-hover:text-[#F6F6F6]/55'>
|
||||
Type II · PHI protected →
|
||||
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em] transition-colors group-hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_75%,transparent)]'>
|
||||
Type II →
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -80,31 +96,31 @@ function TrustStrip() {
|
||||
href='https://github.com/simstudioai/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group flex items-center gap-3 border-[#2A2A2A] border-b px-4 py-[14px] transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
|
||||
className='group flex items-center gap-3 border-[var(--landing-bg-elevated)] border-b px-4 py-3.5 transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
|
||||
>
|
||||
<div className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#FFCC02]/10'>
|
||||
<GithubIcon width={11} height={11} className='text-[#FFCC02]/75' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-[3px]'>
|
||||
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
|
||||
<strong className='font-[430] font-season text-small text-white leading-none'>
|
||||
Open Source
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em] transition-colors group-hover:text-[#F6F6F6]/55'>
|
||||
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em] transition-colors group-hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_75%,transparent)]'>
|
||||
View on GitHub →
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* SSO */}
|
||||
<div className='flex items-center gap-3 px-4 py-[14px]'>
|
||||
<div className='flex items-center gap-3 px-4 py-3.5'>
|
||||
<div className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#2ABBF8]/10'>
|
||||
<Lock className='h-[14px] w-[14px] text-[#2ABBF8]/75' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-[3px]'>
|
||||
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
|
||||
<strong className='font-[430] font-season text-small text-white leading-none'>
|
||||
SSO & SCIM
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em]'>
|
||||
<span className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_55%,transparent)] text-xs leading-none tracking-[0.02em]'>
|
||||
Okta, Azure AD, Google
|
||||
</span>
|
||||
</div>
|
||||
@@ -115,9 +131,13 @@ function TrustStrip() {
|
||||
|
||||
export default function Enterprise() {
|
||||
return (
|
||||
<section id='enterprise' aria-labelledby='enterprise-heading' className='bg-[#F6F6F6]'>
|
||||
<div className='px-4 pt-[60px] pb-[40px] sm:px-8 sm:pt-[80px] sm:pb-0 md:px-[80px] md:pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-[20px]'>
|
||||
<section
|
||||
id='enterprise'
|
||||
aria-labelledby='enterprise-heading'
|
||||
className='bg-[var(--landing-bg-section)]'
|
||||
>
|
||||
<div className='px-4 pt-[60px] pb-10 sm:px-8 sm:pt-20 sm:pb-0 md:px-20 md:pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-5'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
@@ -129,7 +149,7 @@ export default function Enterprise() {
|
||||
|
||||
<h2
|
||||
id='enterprise-heading'
|
||||
className='max-w-[600px] font-[430] font-season text-[#1C1C1C] text-[32px] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
|
||||
className='max-w-[600px] text-balance font-[430] font-season text-[32px] text-[var(--landing-text-dark)] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
|
||||
>
|
||||
Enterprise features for
|
||||
<br />
|
||||
@@ -137,15 +157,15 @@ export default function Enterprise() {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 overflow-hidden rounded-[12px] bg-[#1C1C1C] sm:mt-10 md:mt-12'>
|
||||
<div className='grid grid-cols-1 border-[#2A2A2A] border-b lg:grid-cols-[1fr_420px]'>
|
||||
<div className='mt-8 overflow-hidden rounded-[12px] bg-[var(--landing-bg)] sm:mt-10 md:mt-12'>
|
||||
<div className='grid grid-cols-1 border-[var(--landing-border)] border-b lg:grid-cols-[1fr_420px]'>
|
||||
{/* Audit Trail */}
|
||||
<div className='border-[#2A2A2A] lg:border-r'>
|
||||
<div className='border-[var(--landing-border)] lg:border-r'>
|
||||
<div className='px-6 pt-6 md:px-8 md:pt-8'>
|
||||
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
|
||||
Audit Trail
|
||||
</h3>
|
||||
<p className='mt-2 max-w-[480px] font-[430] font-season text-[#F6F6F6]/50 text-[14px] leading-[150%] tracking-[0.02em]'>
|
||||
<p className='mt-2 max-w-[480px] font-[430] font-season text-[#F6F6F6]/70 text-[14px] leading-[150%] tracking-[0.02em]'>
|
||||
Every action is captured with full actor attribution.
|
||||
</p>
|
||||
</div>
|
||||
@@ -154,12 +174,12 @@ export default function Enterprise() {
|
||||
</div>
|
||||
|
||||
{/* Access Control */}
|
||||
<div className='border-[#2A2A2A] border-t lg:border-t-0'>
|
||||
<div className='border-[var(--landing-border)] border-t lg:border-t-0'>
|
||||
<div className='px-6 pt-6 md:px-8 md:pt-8'>
|
||||
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
|
||||
Access Control
|
||||
</h3>
|
||||
<p className='mt-[6px] font-[430] font-season text-[#F6F6F6]/50 text-[14px] leading-[150%] tracking-[0.02em]'>
|
||||
<p className='mt-1.5 font-[430] font-season text-[#F6F6F6]/70 text-[14px] leading-[150%] tracking-[0.02em]'>
|
||||
Restrict providers, surfaces, and tools per group.
|
||||
</p>
|
||||
</div>
|
||||
@@ -171,27 +191,27 @@ export default function Enterprise() {
|
||||
|
||||
<TrustStrip />
|
||||
|
||||
{/* Scrolling feature ticker */}
|
||||
<div className='relative mt-6 overflow-hidden border-[#2A2A2A] border-t'>
|
||||
<style dangerouslySetInnerHTML={{ __html: MARQUEE_KEYFRAMES }} />
|
||||
{/* Scrolling feature ticker — keyframe loop; pause on hover. Tags use transitions for hover. */}
|
||||
<div className='enterprise-feature-marquee relative mt-6 overflow-hidden border-[var(--landing-bg-elevated)] border-t'>
|
||||
<style dangerouslySetInnerHTML={{ __html: ENTERPRISE_FEATURE_MARQUEE_STYLES }} />
|
||||
{/* Fade edges */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 bottom-0 left-0 z-10 w-16'
|
||||
style={{ background: 'linear-gradient(to right, #1C1C1C, transparent)' }}
|
||||
className='pointer-events-none absolute top-0 bottom-0 left-0 z-10 w-24'
|
||||
style={{ background: 'linear-gradient(to right, var(--landing-bg), transparent)' }}
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 right-0 bottom-0 z-10 w-16'
|
||||
style={{ background: 'linear-gradient(to left, #1C1C1C, transparent)' }}
|
||||
className='pointer-events-none absolute top-0 right-0 bottom-0 z-10 w-24'
|
||||
style={{ background: 'linear-gradient(to left, var(--landing-bg), transparent)' }}
|
||||
/>
|
||||
{/* Duplicate tags for seamless loop */}
|
||||
<div className='flex w-max' style={{ animation: 'marquee 30s linear infinite' }}>
|
||||
<div className='enterprise-feature-marquee-track flex w-max'>
|
||||
{[...FEATURE_TAGS, ...FEATURE_TAGS, ...FEATURE_TAGS, ...FEATURE_TAGS].map(
|
||||
(tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className='whitespace-nowrap border-[#2A2A2A] border-r px-5 py-4 font-[430] font-season text-[#F6F6F6]/40 text-[13px] leading-none tracking-[0.02em]'
|
||||
className='enterprise-feature-marquee-tag whitespace-nowrap border-[var(--landing-bg-elevated)] border-r px-5 py-4 font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-small leading-none tracking-[0.02em] hover:bg-white/[0.04] hover:text-[color-mix(in_srgb,var(--landing-text-subtle)_80%,transparent)]'
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
@@ -200,35 +220,35 @@ export default function Enterprise() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between border-[#2A2A2A] border-t px-6 py-5 md:px-8 md:py-6'>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/40 text-[15px] leading-[150%] tracking-[0.02em]'>
|
||||
<div className='flex items-center justify-between border-[var(--landing-bg-elevated)] border-t px-6 py-5 md:px-8 md:py-6'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-base leading-[150%] tracking-[0.02em]'>
|
||||
Ready for growth?
|
||||
</p>
|
||||
<Link
|
||||
href='https://form.typeform.com/to/jqCO12pF'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group/cta inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-white bg-white px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Book a demo
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
<DemoRequestModal>
|
||||
<button
|
||||
type='button'
|
||||
className='group/cta inline-flex h-[32px] cursor-pointer items-center gap-1.5 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Book a demo
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</Link>
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</DemoRequestModal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -304,7 +304,7 @@ function WorkspacePreview({ activeTab, isActive }: { activeTab: number; isActive
|
||||
|
||||
function MockUserInput({ text }: { text: string }) {
|
||||
return (
|
||||
<div className='flex w-[380px] items-center gap-[6px] rounded-[16px] border border-[#E0E0E0] bg-white px-[10px] py-[8px] shadow-[0_2px_8px_rgba(0,0,0,0.06)]'>
|
||||
<div className='flex w-[380px] items-center gap-1.5 rounded-[16px] border border-[#E0E0E0] bg-white px-2.5 py-2 shadow-[0_2px_8px_rgba(0,0,0,0.06)]'>
|
||||
<div className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-full border border-[#E8E8E8]'>
|
||||
<svg width='12' height='12' viewBox='0 0 12 12' fill='none'>
|
||||
<path d='M6 2.5v7M2.5 6h7' stroke='#999' strokeWidth='1.5' strokeLinecap='round' />
|
||||
@@ -364,7 +364,7 @@ function MiniCardHeader({
|
||||
color?: string
|
||||
}) {
|
||||
return (
|
||||
<div className='flex items-center gap-[4px] border-[#F0F0F0] border-b px-[8px] py-[5px]'>
|
||||
<div className='flex items-center gap-1 border-[#F0F0F0] border-b px-2 py-1.5'>
|
||||
<MiniCardIcon variant={variant} color={color} />
|
||||
<span className='truncate font-medium text-[#888] text-[7px] leading-none'>{label}</span>
|
||||
</div>
|
||||
@@ -419,7 +419,7 @@ function MiniCardBody({ variant, color }: { variant: CardVariant; color?: string
|
||||
|
||||
function PromptCardBody() {
|
||||
return (
|
||||
<div className='px-[8px] py-[6px]'>
|
||||
<div className='px-2 py-1.5'>
|
||||
<p className='break-words text-[#AAAAAA] text-[6.5px] leading-[10px]'>{TYPING_PROMPT}</p>
|
||||
</div>
|
||||
)
|
||||
@@ -427,7 +427,7 @@ function PromptCardBody() {
|
||||
|
||||
function FileCardBody() {
|
||||
return (
|
||||
<div className='flex flex-col gap-[3px] px-[8px] py-[6px]'>
|
||||
<div className='flex flex-col gap-[3px] px-2 py-1.5'>
|
||||
<div className='h-[2px] w-[78%] rounded-full bg-[#E8E8E8]' />
|
||||
<div className='h-[2px] w-[92%] rounded-full bg-[#E8E8E8]' />
|
||||
<div className='h-[2px] w-[62%] rounded-full bg-[#E8E8E8]' />
|
||||
@@ -450,7 +450,7 @@ const TABLE_ROW_WIDTHS = [
|
||||
function TableCardBody() {
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex items-center gap-[4px] bg-[#FAFAFA] px-[6px] py-[3px]'>
|
||||
<div className='flex items-center gap-1 bg-[#FAFAFA] px-1.5 py-[3px]'>
|
||||
<div className='h-[2px] flex-1 rounded-full bg-[#D4D4D4]' />
|
||||
<div className='h-[2px] flex-1 rounded-full bg-[#D4D4D4]' />
|
||||
<div className='h-[2px] flex-1 rounded-full bg-[#D4D4D4]' />
|
||||
@@ -458,7 +458,7 @@ function TableCardBody() {
|
||||
{TABLE_ROW_WIDTHS.map((row, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className='flex items-center gap-[4px] border-[#F5F5F5] border-b px-[6px] py-[3.5px]'
|
||||
className='flex items-center gap-1 border-[#F5F5F5] border-b px-1.5 py-[3.5px]'
|
||||
>
|
||||
<div className='h-[1.5px] rounded-full bg-[#EBEBEB]' style={{ width: `${row[0]}%` }} />
|
||||
<div className='h-[1.5px] rounded-full bg-[#EBEBEB]' style={{ width: `${row[1]}%` }} />
|
||||
@@ -472,17 +472,17 @@ function TableCardBody() {
|
||||
function WorkflowCardBody({ color }: { color: string }) {
|
||||
return (
|
||||
<div className='relative h-full w-full'>
|
||||
<div className='absolute top-[10px] left-[10px] h-[14px] w-[14px] rounded-[3px] border border-[#E0E0E0] bg-[#F8F8F8]' />
|
||||
<div className='absolute top-2.5 left-[10px] h-[14px] w-[14px] rounded-[3px] border border-[#E0E0E0] bg-[#F8F8F8]' />
|
||||
<div className='absolute top-[16px] left-[24px] h-[1px] w-[16px] bg-[#D8D8D8]' />
|
||||
<div
|
||||
className='absolute top-[10px] left-[40px] h-[14px] w-[14px] rounded-[3px] border-[2px]'
|
||||
className='absolute top-2.5 left-[40px] h-[14px] w-[14px] rounded-[3px] border-[2px]'
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
borderColor: `${color}60`,
|
||||
backgroundClip: 'padding-box',
|
||||
}}
|
||||
/>
|
||||
<div className='absolute top-[24px] left-[46px] h-[12px] w-[1px] bg-[#D8D8D8]' />
|
||||
<div className='absolute top-6 left-[46px] h-[12px] w-[1px] bg-[#D8D8D8]' />
|
||||
<div className='absolute top-[36px] left-[40px] h-[14px] w-[14px] rounded-[3px] border border-[#E0E0E0] bg-[#F8F8F8]' />
|
||||
<div className='absolute top-[42px] left-[54px] h-[1px] w-[14px] bg-[#D8D8D8]' />
|
||||
<div
|
||||
@@ -502,9 +502,9 @@ const KB_WIDTHS = [70, 85, 55, 80, 48] as const
|
||||
|
||||
function KnowledgeCardBody() {
|
||||
return (
|
||||
<div className='flex flex-col gap-[5px] px-[8px] py-[6px]'>
|
||||
<div className='flex flex-col gap-[5px] px-2 py-1.5'>
|
||||
{KB_WIDTHS.map((w, i) => (
|
||||
<div key={i} className='flex items-center gap-[4px]'>
|
||||
<div key={i} className='flex items-center gap-1'>
|
||||
<div className='h-[3px] w-[3px] flex-shrink-0 rounded-full bg-[#D4D4D4]' />
|
||||
<div className='h-[1.5px] rounded-full bg-[#E8E8E8]' style={{ width: `${w}%` }} />
|
||||
</div>
|
||||
@@ -524,9 +524,9 @@ const LOG_ENTRIES = [
|
||||
|
||||
function LogsCardBody() {
|
||||
return (
|
||||
<div className='flex flex-col gap-[3px] px-[6px] py-[4px]'>
|
||||
<div className='flex flex-col gap-[3px] px-1.5 py-1'>
|
||||
{LOG_ENTRIES.map((entry, i) => (
|
||||
<div key={i} className='flex items-center gap-[4px] py-[1px]'>
|
||||
<div key={i} className='flex items-center gap-1 py-[1px]'>
|
||||
<div
|
||||
className='h-[3px] w-[3px] flex-shrink-0 rounded-full'
|
||||
style={{ backgroundColor: entry.color }}
|
||||
@@ -608,17 +608,20 @@ const MOCK_KB_DATA = [
|
||||
|
||||
const MD_COMPONENTS: Components = {
|
||||
h1: ({ children }) => (
|
||||
<h1 className='mb-4 border-[#E5E5E5] border-b pb-2 font-semibold text-[#1C1C1C] text-[20px]'>
|
||||
<p
|
||||
role='presentation'
|
||||
className='mb-4 border-[#E5E5E5] border-b pb-2 font-semibold text-[#1C1C1C] text-[20px]'
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
</p>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className='mt-5 mb-3 border-[#E5E5E5] border-b pb-1.5 font-semibold text-[#1C1C1C] text-[16px]'>
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
ul: ({ children }) => <ul className='mb-3 list-disc pl-[24px]'>{children}</ul>,
|
||||
ol: ({ children }) => <ol className='mb-3 list-decimal pl-[24px]'>{children}</ol>,
|
||||
ul: ({ children }) => <ul className='mb-3 list-disc pl-6'>{children}</ul>,
|
||||
ol: ({ children }) => <ol className='mb-3 list-decimal pl-6'>{children}</ol>,
|
||||
li: ({ children }) => (
|
||||
<li className='mb-1 text-[#1C1C1C] text-[14px] leading-[1.6]'>{children}</li>
|
||||
),
|
||||
@@ -630,8 +633,8 @@ function MockFullFiles() {
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<File className='h-[14px] w-[14px] text-[#999]' />
|
||||
<span className='text-[#999] text-[13px]'>Files</span>
|
||||
<span className='text-[#D4D4D4] text-[13px]'>/</span>
|
||||
@@ -651,7 +654,7 @@ function MockFullFiles() {
|
||||
onChange={(e) => setSource(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoCorrect='off'
|
||||
className='h-full w-full resize-none overflow-auto whitespace-pre-wrap bg-transparent p-[24px] font-[300] font-mono text-[#1C1C1C] text-[12px] leading-[1.7] outline-none'
|
||||
className='h-full w-full resize-none overflow-auto whitespace-pre-wrap bg-transparent p-6 font-[300] font-mono text-[#1C1C1C] text-[12px] leading-[1.7] outline-none'
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
@@ -663,7 +666,7 @@ function MockFullFiles() {
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4, delay: 0.5 }}
|
||||
>
|
||||
<div className='h-full overflow-auto p-[24px]'>
|
||||
<div className='h-full overflow-auto p-6'>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={MD_COMPONENTS}>
|
||||
{source}
|
||||
</ReactMarkdown>
|
||||
@@ -683,8 +686,8 @@ const KB_STATUS_STYLES: Record<string, { bg: string; text: string; label: string
|
||||
function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<Database className='h-[14px] w-[14px] text-[#999]' />
|
||||
<span className='text-[#999] text-[13px]'>Knowledge Base</span>
|
||||
<span className='text-[#D4D4D4] text-[13px]'>/</span>
|
||||
@@ -692,12 +695,12 @@ function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex h-[36px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<div className='flex h-[24px] items-center gap-[4px] rounded-[6px] border border-[#E5E5E5] px-[8px] text-[#999] text-[12px]'>
|
||||
<div className='flex h-[36px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<div className='flex h-[24px] items-center gap-1 rounded-[6px] border border-[#E5E5E5] px-2 text-[#999] text-[12px]'>
|
||||
Sort
|
||||
</div>
|
||||
<div className='flex h-[24px] items-center gap-[4px] rounded-[6px] border border-[#E5E5E5] px-[8px] text-[#999] text-[12px]'>
|
||||
<div className='flex h-[24px] items-center gap-1 rounded-[6px] border border-[#E5E5E5] px-2 text-[#999] text-[12px]'>
|
||||
Filter
|
||||
</div>
|
||||
</div>
|
||||
@@ -713,7 +716,7 @@ function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-[4px] py-[7px] text-center align-middle'>
|
||||
<th className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-1 py-[7px] text-center align-middle'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<div className='h-[13px] w-[13px] rounded-[2px] border border-[#D4D4D4]' />
|
||||
</div>
|
||||
@@ -721,7 +724,7 @@ function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
|
||||
{MOCK_KB_COLUMNS.map((col) => (
|
||||
<th
|
||||
key={col}
|
||||
className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-[8px] py-[7px] text-left align-middle'
|
||||
className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-2 py-[7px] text-left align-middle'
|
||||
>
|
||||
<span className='font-base text-[#999] text-[13px]'>{col}</span>
|
||||
</th>
|
||||
@@ -739,11 +742,11 @@ function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
>
|
||||
<td className='border-[#E5E5E5] border-r border-b px-[4px] py-[7px] text-center align-middle'>
|
||||
<td className='border-[#E5E5E5] border-r border-b px-1 py-[7px] text-center align-middle'>
|
||||
<span className='text-[#999] text-[11px] tabular-nums'>{i + 1}</span>
|
||||
</td>
|
||||
<td className='border-[#E5E5E5] border-r border-b px-[8px] py-[7px] align-middle'>
|
||||
<span className='flex items-center gap-[8px] text-[#1C1C1C] text-[13px]'>
|
||||
<td className='border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle'>
|
||||
<span className='flex items-center gap-2 text-[#1C1C1C] text-[13px]'>
|
||||
<DocIcon className='h-[14px] w-[14px] shrink-0' />
|
||||
<span className='truncate'>{row[0]}</span>
|
||||
</span>
|
||||
@@ -751,14 +754,14 @@ function MockFullKnowledgeBase({ revealedRows }: { revealedRows: number }) {
|
||||
{row.slice(1, 4).map((cell, j) => (
|
||||
<td
|
||||
key={j}
|
||||
className='border-[#E5E5E5] border-r border-b px-[8px] py-[7px] align-middle'
|
||||
className='border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle'
|
||||
>
|
||||
<span className='text-[#999] text-[13px]'>{cell}</span>
|
||||
</td>
|
||||
))}
|
||||
<td className='border-[#E5E5E5] border-r border-b px-[8px] py-[7px] align-middle'>
|
||||
<td className='border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle'>
|
||||
<span
|
||||
className='inline-flex items-center rounded-full px-[8px] py-[2px] font-medium text-[11px]'
|
||||
className='inline-flex items-center rounded-full px-2 py-0.5 font-medium text-[11px]'
|
||||
style={{ backgroundColor: status.bg, color: status.text }}
|
||||
>
|
||||
{status.label}
|
||||
@@ -882,8 +885,8 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
|
||||
return (
|
||||
<div className='relative flex h-full'>
|
||||
<div className='flex min-w-0 flex-1 flex-col'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<Library className='h-[14px] w-[14px] text-[#999]' />
|
||||
<span className='font-medium text-[#1C1C1C] text-[13px]'>Logs</span>
|
||||
</div>
|
||||
@@ -899,7 +902,7 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
|
||||
<thead className='shadow-[inset_0_-1px_0_#E5E5E5]'>
|
||||
<tr>
|
||||
{['Workflow', 'Date', 'Status', 'Cost', 'Trigger', 'Duration'].map((col) => (
|
||||
<th key={col} className='h-10 px-[24px] py-[10px] text-left align-middle'>
|
||||
<th key={col} className='h-10 px-6 py-2.5 text-left align-middle'>
|
||||
<span className='font-base text-[#999] text-[13px]'>{col}</span>
|
||||
</th>
|
||||
))}
|
||||
@@ -921,8 +924,8 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
|
||||
)}
|
||||
onClick={() => setSelectedRow(i)}
|
||||
>
|
||||
<td className='px-[24px] py-[10px] align-middle'>
|
||||
<span className='flex items-center gap-[12px] font-medium text-[#1C1C1C] text-[14px]'>
|
||||
<td className='px-6 py-2.5 align-middle'>
|
||||
<span className='flex items-center gap-3 font-medium text-[#1C1C1C] text-[14px]'>
|
||||
<div
|
||||
className='h-[10px] w-[10px] shrink-0 rounded-[3px] border-[1.5px]'
|
||||
style={{
|
||||
@@ -934,26 +937,26 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
|
||||
<span className='truncate'>{row[0]}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td className='px-[24px] py-[10px] align-middle'>
|
||||
<td className='px-6 py-2.5 align-middle'>
|
||||
<span className='font-medium text-[#999] text-[14px]'>{row[1]}</span>
|
||||
</td>
|
||||
<td className='px-[24px] py-[10px] align-middle'>
|
||||
<td className='px-6 py-2.5 align-middle'>
|
||||
<span
|
||||
className='inline-flex items-center rounded-full px-[8px] py-[2px] font-medium text-[11px]'
|
||||
className='inline-flex items-center rounded-full px-2 py-0.5 font-medium text-[11px]'
|
||||
style={{ backgroundColor: statusStyle.bg, color: statusStyle.text }}
|
||||
>
|
||||
{statusStyle.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className='px-[24px] py-[10px] align-middle'>
|
||||
<td className='px-6 py-2.5 align-middle'>
|
||||
<span className='font-medium text-[#999] text-[14px]'>{row[3]}</span>
|
||||
</td>
|
||||
<td className='px-[24px] py-[10px] align-middle'>
|
||||
<span className='rounded-[4px] bg-[#F5F5F5] px-[6px] py-[2px] text-[#666] text-[11px]'>
|
||||
<td className='px-6 py-2.5 align-middle'>
|
||||
<span className='rounded-[4px] bg-[#F5F5F5] px-1.5 py-0.5 text-[#666] text-[11px]'>
|
||||
{row[4]}
|
||||
</span>
|
||||
</td>
|
||||
<td className='px-[24px] py-[10px] align-middle'>
|
||||
<td className='px-6 py-2.5 align-middle'>
|
||||
<span className='font-medium text-[#999] text-[14px]'>{row[5]}</span>
|
||||
</td>
|
||||
</motion.tr>
|
||||
@@ -998,7 +1001,7 @@ function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSi
|
||||
const isNextDisabled = selectedRow === MOCK_LOG_DATA.length - 1
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col overflow-y-auto px-[14px] pt-[12px]'>
|
||||
<div className='flex h-full flex-col overflow-y-auto px-3.5 pt-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='font-medium text-[#1C1C1C] text-[14px]'>Log Details</span>
|
||||
<div className='flex items-center gap-[1px]'>
|
||||
@@ -1027,18 +1030,18 @@ function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-[20px] flex flex-col gap-[10px]'>
|
||||
<div className='flex items-center gap-[16px] px-[1px]'>
|
||||
<div className='flex w-[120px] shrink-0 flex-col gap-[8px]'>
|
||||
<div className='mt-5 flex flex-col gap-2.5'>
|
||||
<div className='flex items-center gap-4 px-[1px]'>
|
||||
<div className='flex w-[120px] shrink-0 flex-col gap-2'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Timestamp</span>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<span className='font-medium text-[#666] text-[13px]'>{date}</span>
|
||||
<span className='font-medium text-[#666] text-[13px]'>{time}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-[8px]'>
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-2'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Workflow</span>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div
|
||||
className='h-[10px] w-[10px] shrink-0 rounded-[3px] border-[1.5px]'
|
||||
style={{
|
||||
@@ -1053,42 +1056,39 @@ function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSi
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex h-[42px] items-center justify-between border-[#E5E5E5] border-b px-[8px]'>
|
||||
<div className='flex h-[42px] items-center justify-between border-[#E5E5E5] border-b px-2'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Level</span>
|
||||
<span
|
||||
className='inline-flex items-center rounded-full px-[8px] py-[2px] font-medium text-[11px]'
|
||||
className='inline-flex items-center rounded-full px-2 py-0.5 font-medium text-[11px]'
|
||||
style={{ backgroundColor: statusStyle.bg, color: statusStyle.text }}
|
||||
>
|
||||
{statusStyle.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex h-[42px] items-center justify-between border-[#E5E5E5] border-b px-[8px]'>
|
||||
<div className='flex h-[42px] items-center justify-between border-[#E5E5E5] border-b px-2'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Trigger</span>
|
||||
<span className='rounded-[4px] bg-[#F5F5F5] px-[6px] py-[2px] text-[#666] text-[11px]'>
|
||||
<span className='rounded-[4px] bg-[#F5F5F5] px-1.5 py-0.5 text-[#666] text-[11px]'>
|
||||
{row[4]}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex h-[42px] items-center justify-between px-[8px]'>
|
||||
<div className='flex h-[42px] items-center justify-between px-2'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Duration</span>
|
||||
<span className='font-medium text-[#666] text-[13px]'>{row[5]}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[6px] rounded-[6px] border border-[#E5E5E5] bg-[#FAFAFA] px-[10px] py-[8px]'>
|
||||
<div className='flex flex-col gap-1.5 rounded-[6px] border border-[#E5E5E5] bg-[#FAFAFA] px-2.5 py-2'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Workflow Output</span>
|
||||
<div className='rounded-[6px] bg-[#F0F0F0] p-[10px] font-mono text-[#555] text-[11px] leading-[1.5]'>
|
||||
<div className='rounded-[6px] bg-[#F0F0F0] p-2.5 font-mono text-[#555] text-[11px] leading-[1.5]'>
|
||||
{detail.output}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[6px] rounded-[6px] border border-[#E5E5E5] bg-[#FAFAFA] px-[10px] py-[8px]'>
|
||||
<div className='flex flex-col gap-1.5 rounded-[6px] border border-[#E5E5E5] bg-[#FAFAFA] px-2.5 py-2'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Trace Spans</span>
|
||||
<div className='flex flex-col gap-[6px]'>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
{detail.spans.map((span, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn('flex flex-col gap-[3px]', span.depth === 1 && 'ml-[12px]')}
|
||||
>
|
||||
<div key={i} className={cn('flex flex-col gap-[3px]', span.depth === 1 && 'ml-3')}>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='font-mono text-[#555] text-[11px]'>{span.name}</span>
|
||||
<span className='font-medium text-[#999] text-[11px]'>{span.ms}ms</span>
|
||||
@@ -1113,8 +1113,8 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<Table className='h-[14px] w-[14px] text-[#999]' />
|
||||
<span className='text-[#999] text-[13px]'>Tables</span>
|
||||
<span className='text-[#D4D4D4] text-[13px]'>/</span>
|
||||
@@ -1122,12 +1122,12 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex h-[36px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<div className='flex h-[24px] items-center gap-[4px] rounded-[6px] border border-[#E5E5E5] px-[8px] text-[#999] text-[12px]'>
|
||||
<div className='flex h-[36px] shrink-0 items-center border-[#E5E5E5] border-b px-6'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<div className='flex h-[24px] items-center gap-1 rounded-[6px] border border-[#E5E5E5] px-2 text-[#999] text-[12px]'>
|
||||
Sort
|
||||
</div>
|
||||
<div className='flex h-[24px] items-center gap-[4px] rounded-[6px] border border-[#E5E5E5] px-[8px] text-[#999] text-[12px]'>
|
||||
<div className='flex h-[24px] items-center gap-1 rounded-[6px] border border-[#E5E5E5] px-2 text-[#999] text-[12px]'>
|
||||
Filter
|
||||
</div>
|
||||
</div>
|
||||
@@ -1143,7 +1143,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-[4px] py-[7px] text-center align-middle'>
|
||||
<th className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-1 py-[7px] text-center align-middle'>
|
||||
<div className='flex items-center justify-center'>
|
||||
<div className='h-[13px] w-[13px] rounded-[2px] border border-[#D4D4D4]' />
|
||||
</div>
|
||||
@@ -1151,9 +1151,9 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
|
||||
{MOCK_TABLE_COLUMNS.map((col) => (
|
||||
<th
|
||||
key={col}
|
||||
className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-[8px] py-[7px] text-left align-middle'
|
||||
className='border-[#E5E5E5] border-r border-b bg-[#FAFAFA] px-2 py-[7px] text-left align-middle'
|
||||
>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<ColumnTypeIcon />
|
||||
<span className='font-medium text-[#1C1C1C] text-[13px]'>{col}</span>
|
||||
<ChevronDown className='ml-auto h-[7px] w-[9px] shrink-0 text-[#CCC]' />
|
||||
@@ -1176,7 +1176,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
|
||||
>
|
||||
<td
|
||||
className={cn(
|
||||
'border-[#E5E5E5] border-r border-b px-[4px] py-[7px] text-center align-middle',
|
||||
'border-[#E5E5E5] border-r border-b px-1 py-[7px] text-center align-middle',
|
||||
isSelected ? 'bg-[rgba(37,99,235,0.06)]' : 'hover:bg-[#FAFAFA]'
|
||||
)}
|
||||
>
|
||||
@@ -1186,7 +1186,7 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
|
||||
<td
|
||||
key={j}
|
||||
className={cn(
|
||||
'relative border-[#E5E5E5] border-r border-b px-[8px] py-[7px] align-middle',
|
||||
'relative border-[#E5E5E5] border-r border-b px-2 py-[7px] align-middle',
|
||||
isSelected ? 'bg-[rgba(37,99,235,0.06)]' : 'hover:bg-[#FAFAFA]'
|
||||
)}
|
||||
>
|
||||
@@ -1308,13 +1308,13 @@ function DefaultPreview() {
|
||||
return (
|
||||
<motion.div
|
||||
key={key}
|
||||
className='absolute flex items-center justify-center rounded-xl border border-[#E5E5E5] bg-white p-[10px] shadow-[0_2px_4px_0_rgba(0,0,0,0.06)]'
|
||||
className='absolute flex items-center justify-center rounded-xl border border-[#E5E5E5] bg-white p-2.5 shadow-[0_2px_4px_0_rgba(0,0,0,0.06)]'
|
||||
initial={{ top: '50%', left: '50%', opacity: 0, scale: 0, x: '-50%', y: '-50%' }}
|
||||
animate={inView ? { top, left, opacity: 1, scale: 1, x: '-50%', y: '-50%' } : undefined}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 50,
|
||||
damping: 12,
|
||||
stiffness: 80,
|
||||
damping: 20,
|
||||
delay: explodeDelay,
|
||||
}}
|
||||
style={{ color }}
|
||||
@@ -1331,7 +1331,7 @@ function DefaultPreview() {
|
||||
animate={inView ? { opacity: 1, x: '-50%', y: '-50%' } : undefined}
|
||||
transition={{ duration: 0.4, ease: 'easeOut', delay: 0 }}
|
||||
>
|
||||
<div className='flex h-[36px] items-center gap-[8px] rounded-[8px] border border-[#E5E5E5] bg-white px-[10px] shadow-[0_2px_6px_0_rgba(0,0,0,0.08)]'>
|
||||
<div className='flex h-[36px] items-center gap-2 rounded-[8px] border border-[#E5E5E5] bg-white px-2.5 shadow-[0_2px_6px_0_rgba(0,0,0,0.08)]'>
|
||||
<div className='flex h-[22px] w-[22px] flex-shrink-0 items-center justify-center rounded-[5px] bg-[#1e1e1e]'>
|
||||
<svg width='11' height='11' viewBox='0 0 10 10' fill='none'>
|
||||
<path
|
||||
|
||||
@@ -152,7 +152,7 @@ function DotGrid({
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className={`h-full shrink-0 bg-[#F6F6F6] p-[6px] ${borderLeft ? 'border-[#E9E9E9] border-l' : ''}`}
|
||||
className={`h-full shrink-0 bg-[var(--landing-bg-section)] p-1.5 ${borderLeft ? 'border-[var(--divider)] border-l' : ''}`}
|
||||
style={{
|
||||
width: width ? `${width}px` : undefined,
|
||||
display: 'grid',
|
||||
@@ -181,7 +181,7 @@ export default function Features() {
|
||||
<section
|
||||
id='features'
|
||||
aria-labelledby='features-heading'
|
||||
className='relative overflow-hidden bg-[#F6F6F6]'
|
||||
className='relative overflow-hidden bg-[var(--landing-bg-section)]'
|
||||
>
|
||||
<div aria-hidden='true' className='absolute top-0 left-0 w-full'>
|
||||
<Image
|
||||
@@ -190,15 +190,11 @@ export default function Features() {
|
||||
width={1440}
|
||||
height={366}
|
||||
className='h-auto w-full'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 pt-[60px] lg:pt-[100px]'>
|
||||
<div
|
||||
ref={sectionRef}
|
||||
className='flex flex-col items-start gap-[20px] px-[24px] lg:px-[80px]'
|
||||
>
|
||||
<div ref={sectionRef} className='flex flex-col items-start gap-5 px-6 lg:px-20'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
@@ -216,31 +212,31 @@ export default function Features() {
|
||||
</Badge>
|
||||
<h2
|
||||
id='features-heading'
|
||||
className='max-w-[900px] font-[430] font-season text-[#1C1C1C] text-[28px] leading-[110%] tracking-[-0.02em] md:text-[40px]'
|
||||
className='max-w-[900px] text-balance font-[430] font-season text-[28px] text-[var(--landing-text-dark)] leading-[110%] tracking-[-0.02em] md:text-[40px]'
|
||||
>
|
||||
{HEADING_LETTERS.map((char, i) => (
|
||||
<ScrollLetter key={i} scrollYProgress={scrollYProgress} charIndex={i}>
|
||||
{char}
|
||||
</ScrollLetter>
|
||||
))}
|
||||
<span className='text-[#1C1C1C]/40'>
|
||||
<span className='text-[color-mix(in_srgb,var(--landing-text-dark)_40%,transparent)]'>
|
||||
Design powerful workflows, connect your data, and monitor every run — all in one
|
||||
platform.
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='relative mt-[40px] pb-[40px] lg:mt-[73px] lg:pb-[80px]'>
|
||||
<div className='relative mt-10 pb-10 lg:mt-[73px] lg:pb-20'>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 bottom-0 left-[80px] z-20 hidden w-px bg-[#E9E9E9] lg:block'
|
||||
className='absolute top-0 bottom-0 left-[80px] z-20 hidden w-px bg-[var(--divider)] lg:block'
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 right-[80px] bottom-0 z-20 hidden w-px bg-[#E9E9E9] lg:block'
|
||||
className='absolute top-0 right-[80px] bottom-0 z-20 hidden w-px bg-[var(--divider)] lg:block'
|
||||
/>
|
||||
|
||||
<div className='flex h-[68px] border border-[#E9E9E9] lg:overflow-hidden'>
|
||||
<div className='flex h-[68px] border border-[var(--divider)] lg:overflow-hidden'>
|
||||
<div className='h-full shrink-0'>
|
||||
<div className='h-full lg:hidden'>
|
||||
<DotGrid cols={3} rows={8} width={24} />
|
||||
@@ -258,7 +254,7 @@ export default function Features() {
|
||||
role='tab'
|
||||
aria-selected={index === activeTab}
|
||||
onClick={() => setActiveTab(index)}
|
||||
className={`relative h-full flex-1 items-center justify-center whitespace-nowrap px-[12px] font-medium font-season text-[#212121] text-[12px] uppercase lg:px-0 lg:text-[14px]${tab.hideOnMobile ? ' hidden lg:flex' : ' flex'}${index > 0 ? ' border-[#E9E9E9] border-l' : ''}`}
|
||||
className={`relative h-full flex-1 items-center justify-center whitespace-nowrap px-3 font-medium font-season text-[var(--landing-text-dark)] text-caption uppercase lg:px-0 lg:text-sm${tab.hideOnMobile ? ' hidden lg:flex' : ' flex'}${index > 0 ? ' border-[var(--divider)] border-l' : ''}`}
|
||||
style={{ backgroundColor: index === activeTab ? '#FDFDFD' : '#F6F6F6' }}
|
||||
>
|
||||
{tab.mobileLabel ? (
|
||||
@@ -298,19 +294,19 @@ export default function Features() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-[32px] flex flex-col gap-[24px] px-[24px] lg:mt-[60px] lg:grid lg:grid-cols-[1fr_2.8fr] lg:gap-[60px] lg:px-[120px]'>
|
||||
<div className='flex flex-col items-start justify-between gap-[24px] pt-[20px] lg:h-[560px] lg:gap-0'>
|
||||
<div className='flex flex-col items-start gap-[16px]'>
|
||||
<h3 className='font-[430] font-season text-[#1C1C1C] text-[24px] leading-[120%] tracking-[-0.02em] lg:text-[28px]'>
|
||||
<div className='mt-8 flex flex-col gap-6 px-6 lg:mt-[60px] lg:grid lg:grid-cols-[1fr_2.8fr] lg:gap-[60px] lg:px-[120px]'>
|
||||
<div className='flex flex-col items-start justify-between gap-6 pt-5 lg:h-[560px] lg:gap-0'>
|
||||
<div className='flex flex-col items-start gap-4'>
|
||||
<h3 className='font-[430] font-season text-[24px] text-[var(--landing-text-dark)] leading-[120%] tracking-[-0.02em] lg:text-[28px]'>
|
||||
{FEATURE_TABS[activeTab].title}
|
||||
</h3>
|
||||
<p className='font-[430] font-season text-[#1C1C1C]/50 text-[16px] leading-[150%] tracking-[0.02em] lg:text-[18px]'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-dark)_50%,transparent)] text-md leading-[150%] tracking-[0.02em] lg:text-lg'>
|
||||
{FEATURE_TABS[activeTab].description}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href='/signup'
|
||||
className='group/cta inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-[10px] font-[430] font-season text-[14px] text-white transition-colors hover:border-[#2A2A2A] hover:bg-[#2A2A2A]'
|
||||
className='group/cta inline-flex h-[32px] items-center gap-1.5 rounded-[5px] border border-[#1D1D1D] bg-[#1D1D1D] px-2.5 font-[430] font-season text-sm text-white transition-colors hover:border-[var(--landing-bg-elevated)] hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
{FEATURE_TABS[activeTab].cta}
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
@@ -336,7 +332,7 @@ export default function Features() {
|
||||
<FeaturesPreview activeTab={activeTab} />
|
||||
</div>
|
||||
|
||||
<div aria-hidden='true' className='mt-[60px] hidden h-px bg-[#E9E9E9] lg:block' />
|
||||
<div aria-hidden='true' className='mt-[60px] hidden h-px bg-[var(--divider)] lg:block' />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useCallback, useRef, useState } from 'react'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useLandingSubmit } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { useAnimatedPlaceholder } from '@/app/workspace/[workspaceId]/home/hooks/use-animated-placeholder'
|
||||
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
|
||||
|
||||
const MAX_HEIGHT = 120
|
||||
|
||||
const CTA_BUTTON =
|
||||
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
|
||||
'inline-flex items-center h-[32px] rounded-[5px] border px-2.5 font-[430] font-season text-sm'
|
||||
|
||||
export function FooterCTA() {
|
||||
const landingSubmit = useLandingSubmit()
|
||||
@@ -41,14 +41,14 @@ export function FooterCTA() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center px-4 pt-[120px] pb-[100px] sm:px-8 md:px-[80px]'>
|
||||
<h2 className='text-center font-[430] font-season text-[#1C1C1C] text-[28px] leading-[100%] tracking-[-0.02em] sm:text-[32px] md:text-[36px]'>
|
||||
<div className='flex flex-col items-center px-4 pt-[120px] pb-[100px] sm:px-8 md:px-20'>
|
||||
<h2 className='text-balance text-center font-[430] font-season text-[28px] text-[var(--landing-text-dark)] leading-[100%] tracking-[-0.02em] sm:text-[32px] md:text-[36px]'>
|
||||
What should we get done?
|
||||
</h2>
|
||||
|
||||
<div className='mt-8 w-full max-w-[42rem]'>
|
||||
<div
|
||||
className='cursor-text rounded-[20px] border border-[#E5E5E5] bg-white px-[10px] py-[8px] shadow-sm'
|
||||
className='cursor-text rounded-[20px] border border-[var(--landing-bg-skeleton)] bg-white px-2.5 py-2 shadow-sm'
|
||||
onClick={() => textareaRef.current?.focus()}
|
||||
>
|
||||
<textarea
|
||||
@@ -59,7 +59,7 @@ export function FooterCTA() {
|
||||
onInput={handleInput}
|
||||
placeholder={animatedPlaceholder}
|
||||
rows={2}
|
||||
className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-[4px] py-[4px] font-body text-[#1C1C1C] text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#999] focus-visible:ring-0'
|
||||
className='m-0 box-border min-h-[48px] w-full resize-none border-0 bg-transparent px-1 py-1 font-body text-[var(--landing-text-dark)] text-base leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[var(--landing-text-muted)] focus-visible:ring-0'
|
||||
style={{ caretColor: '#1C1C1C', maxHeight: `${MAX_HEIGHT}px` }}
|
||||
/>
|
||||
<div className='flex items-center justify-end'>
|
||||
@@ -67,6 +67,7 @@ export function FooterCTA() {
|
||||
type='button'
|
||||
onClick={handleSubmit}
|
||||
disabled={isEmpty}
|
||||
aria-label='Submit message'
|
||||
className='flex h-[28px] w-[28px] items-center justify-center rounded-full border-0 p-0 transition-colors'
|
||||
style={{
|
||||
background: isEmpty ? '#C0C0C0' : '#1C1C1C',
|
||||
@@ -79,18 +80,18 @@ export function FooterCTA() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 flex gap-[8px]'>
|
||||
<div className='mt-8 flex gap-2'>
|
||||
<a
|
||||
href='https://docs.sim.ai'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={`${CTA_BUTTON} border-[#D4D4D4] text-[#1C1C1C] transition-colors hover:bg-[#E8E8E8]`}
|
||||
className={`${CTA_BUTTON} border-[var(--landing-border-subtle)] text-[var(--landing-text-dark)] transition-colors hover:bg-[var(--landing-bg-skeleton)]`}
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<Link
|
||||
href='/signup'
|
||||
className={`${CTA_BUTTON} gap-[8px] border-[#1C1C1C] bg-[#1C1C1C] text-white transition-colors hover:border-[#333] hover:bg-[#333]`}
|
||||
className={`${CTA_BUTTON} gap-2 border-[var(--landing-bg)] bg-[var(--landing-bg)] text-white transition-colors hover:border-[var(--landing-bg-elevated)] hover:bg-[var(--landing-bg-elevated)]`}
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
|
||||
@@ -2,7 +2,8 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { FooterCTA } from '@/app/(home)/components/footer/footer-cta'
|
||||
|
||||
const LINK_CLASS = 'text-[14px] text-[#999] transition-colors hover:text-[#ECECEC]'
|
||||
const LINK_CLASS =
|
||||
'text-sm text-[var(--landing-text-muted)] transition-colors hover:text-[var(--landing-text)]'
|
||||
|
||||
interface FooterItem {
|
||||
label: string
|
||||
@@ -25,6 +26,9 @@ const RESOURCES_LINKS: FooterItem[] = [
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
// { label: 'Templates', href: '/templates' },
|
||||
{ label: 'Docs', href: 'https://docs.sim.ai', external: true },
|
||||
{ label: 'Models', href: '/models' },
|
||||
// { label: 'Academy', href: '/academy' },
|
||||
{ label: 'Partners', href: '/partners' },
|
||||
{ label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true },
|
||||
{ label: 'Changelog', href: '/changelog' },
|
||||
]
|
||||
@@ -81,8 +85,8 @@ const LEGAL_LINKS: FooterItem[] = [
|
||||
function FooterColumn({ title, items }: { title: string; items: FooterItem[] }) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className='mb-[16px] font-medium text-[#ECECEC] text-[14px]'>{title}</h3>
|
||||
<div className='flex flex-col gap-[10px]'>
|
||||
<h3 className='mb-4 font-medium text-[var(--landing-text)] text-sm'>{title}</h3>
|
||||
<div className='flex flex-col gap-2.5'>
|
||||
{items.map(({ label, href, external }) =>
|
||||
external ? (
|
||||
<a
|
||||
@@ -113,16 +117,16 @@ export default function Footer({ hideCTA }: FooterProps) {
|
||||
return (
|
||||
<footer
|
||||
role='contentinfo'
|
||||
className={`bg-[#F6F6F6] pb-[40px] font-[430] font-season text-[14px]${hideCTA ? ' pt-[40px]' : ''}`}
|
||||
className={`bg-[var(--landing-bg-section)] pb-10 font-[430] font-season text-sm${hideCTA ? ' pt-10' : ''}`}
|
||||
>
|
||||
{!hideCTA && <FooterCTA />}
|
||||
<div className='px-4 sm:px-8 md:px-[80px]'>
|
||||
<div className='relative overflow-hidden rounded-lg bg-[#1C1C1C] px-6 pt-[40px] pb-[32px] sm:px-10 sm:pt-[48px] sm:pb-[40px]'>
|
||||
<div className='px-4 sm:px-8 md:px-20'>
|
||||
<div className='relative overflow-hidden rounded-lg bg-[var(--landing-bg)] px-6 pt-10 pb-8 sm:px-10 sm:pt-12 sm:pb-10'>
|
||||
<nav
|
||||
aria-label='Footer navigation'
|
||||
className='relative z-[1] grid grid-cols-2 gap-x-8 gap-y-10 sm:grid-cols-3 lg:grid-cols-7'
|
||||
>
|
||||
<div className='col-span-2 flex flex-col gap-[24px] sm:col-span-1'>
|
||||
<div className='col-span-2 flex flex-col gap-6 sm:col-span-1'>
|
||||
<Link href='/' aria-label='Sim home'>
|
||||
<Image
|
||||
src='/logo/sim-landing.svg'
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { DemoRequestModal } from '@/app/(home)/components/demo-request/demo-request-modal'
|
||||
import {
|
||||
BlocksLeftAnimated,
|
||||
BlocksRightAnimated,
|
||||
@@ -19,13 +20,13 @@ const LandingPreview = dynamic(
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <div className='aspect-[1116/549] w-full rounded bg-[#1b1b1b]' />,
|
||||
loading: () => <div className='aspect-[1116/549] w-full rounded bg-[var(--landing-bg)]' />,
|
||||
}
|
||||
)
|
||||
|
||||
/** Shared base classes for CTA link buttons — matches Deploy/Run button styling in the preview panel. */
|
||||
const CTA_BASE =
|
||||
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
|
||||
'inline-flex items-center h-[32px] rounded-[5px] border px-2.5 font-[430] font-season text-sm'
|
||||
|
||||
export default function Hero() {
|
||||
const blockStates = useBlockCycle()
|
||||
@@ -34,14 +35,14 @@ export default function Hero() {
|
||||
<section
|
||||
id='hero'
|
||||
aria-labelledby='hero-heading'
|
||||
className='relative flex flex-col items-center overflow-hidden bg-[#1C1C1C] pt-[60px] pb-[12px] lg:pt-[100px]'
|
||||
className='relative flex flex-col items-center overflow-hidden bg-[var(--landing-bg)] pt-[60px] pb-3 lg:pt-[100px]'
|
||||
>
|
||||
<p className='sr-only'>
|
||||
Sim is the open-source platform to build AI agents and run your agentic workforce. Connect
|
||||
1,000+ integrations and LLMs — including OpenAI, Claude, Gemini, Mistral, and xAI — to
|
||||
deploy and orchestrate agentic workflows. Create agents, workflows, knowledge bases, tables,
|
||||
and docs. Trusted by over 100,000 builders at startups and Fortune 500 companies. SOC2 and
|
||||
HIPAA compliant.
|
||||
SOC2 compliant.
|
||||
</p>
|
||||
|
||||
<div
|
||||
@@ -58,30 +59,30 @@ export default function Hero() {
|
||||
<Image src='/landing/card-right.svg' alt='' fill className='object-contain' />
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 flex flex-col items-center gap-[12px]'>
|
||||
<div className='relative z-10 flex flex-col items-center gap-3'>
|
||||
<h1
|
||||
id='hero-heading'
|
||||
className='font-[430] font-season text-[36px] text-white leading-[100%] tracking-[-0.02em] sm:text-[48px] lg:text-[72px]'
|
||||
className='text-balance font-[430] font-season text-[36px] text-white leading-[100%] tracking-[-0.02em] sm:text-[48px] lg:text-[72px]'
|
||||
>
|
||||
Build AI Agents
|
||||
</h1>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/60 text-[15px] leading-[125%] tracking-[0.02em] lg:text-[18px]'>
|
||||
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-base leading-[125%] tracking-[0.02em] lg:text-lg'>
|
||||
Sim is the AI Workspace for Agent Builders.
|
||||
</p>
|
||||
|
||||
<div className='mt-[12px] flex items-center gap-[8px]'>
|
||||
<a
|
||||
href='https://form.typeform.com/to/jqCO12pF'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={`${CTA_BASE} border-[#3d3d3d] text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
|
||||
aria-label='Get a demo'
|
||||
>
|
||||
Get a demo
|
||||
</a>
|
||||
<div className='mt-3 flex items-center gap-2'>
|
||||
<DemoRequestModal>
|
||||
<button
|
||||
type='button'
|
||||
className={`${CTA_BASE} border-[var(--landing-border-strong)] bg-transparent text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]`}
|
||||
aria-label='Get a demo'
|
||||
>
|
||||
Get a demo
|
||||
</button>
|
||||
</DemoRequestModal>
|
||||
<Link
|
||||
href='/signup'
|
||||
className={`${CTA_BASE} gap-[8px] border-[#FFFFFF] bg-[#FFFFFF] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}
|
||||
className={`${CTA_BASE} gap-2 border-white bg-white text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]`}
|
||||
aria-label='Get started with Sim'
|
||||
>
|
||||
Get started
|
||||
@@ -118,7 +119,7 @@ export default function Hero() {
|
||||
<BlocksRightSideAnimated animState={blockStates.rightSide} />
|
||||
</div>
|
||||
|
||||
<div className='relative z-10 overflow-hidden rounded border border-[#2A2A2A]'>
|
||||
<div className='relative z-10 overflow-hidden rounded border border-[var(--landing-bg-elevated)]'>
|
||||
<LandingPreview />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { File } from '@/components/emcn/icons'
|
||||
import { DocxIcon, PdfIcon } from '@/components/icons/document-icons'
|
||||
import type {
|
||||
PreviewColumn,
|
||||
PreviewRow,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
import {
|
||||
LandingPreviewResource,
|
||||
ownerCell,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
|
||||
/** Generic audio/zip icon using basic SVG since no dedicated component exists */
|
||||
function AudioIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
className={className}
|
||||
>
|
||||
<path d='M9 18V5l12-2v13' />
|
||||
<circle cx='6' cy='18' r='3' />
|
||||
<circle cx='18' cy='16' r='3' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function JsonlIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
className={className}
|
||||
>
|
||||
<path d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' />
|
||||
<path d='M14 2v4a2 2 0 0 0 2 2h4' />
|
||||
<path d='M10 9H8' />
|
||||
<path d='M16 13H8' />
|
||||
<path d='M16 17H8' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ZipIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
className={className}
|
||||
>
|
||||
<path d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' />
|
||||
<path d='M14 2v4a2 2 0 0 0 2 2h4' />
|
||||
<path d='M10 6h1' />
|
||||
<path d='M10 10h1' />
|
||||
<path d='M10 14h1' />
|
||||
<path d='M9 18h2v2h-2z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const COLUMNS: PreviewColumn[] = [
|
||||
{ id: 'name', header: 'Name' },
|
||||
{ id: 'size', header: 'Size' },
|
||||
{ id: 'type', header: 'Type' },
|
||||
{ id: 'created', header: 'Created' },
|
||||
{ id: 'owner', header: 'Owner' },
|
||||
]
|
||||
|
||||
const ROWS: PreviewRow[] = [
|
||||
{
|
||||
id: '1',
|
||||
cells: {
|
||||
name: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'Q1 Performance Report.pdf' },
|
||||
size: { label: '2.4 MB' },
|
||||
type: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'PDF' },
|
||||
created: { label: '3 hours ago' },
|
||||
owner: ownerCell('T', 'Theo L.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: {
|
||||
name: { icon: <ZipIcon className='h-[14px] w-[14px]' />, label: 'product-screenshots.zip' },
|
||||
size: { label: '18.7 MB' },
|
||||
type: { icon: <ZipIcon className='h-[14px] w-[14px]' />, label: 'ZIP' },
|
||||
created: { label: '1 day ago' },
|
||||
owner: ownerCell('A', 'Alex M.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: {
|
||||
name: { icon: <JsonlIcon className='h-[14px] w-[14px]' />, label: 'training-dataset.jsonl' },
|
||||
size: { label: '892 KB' },
|
||||
type: { icon: <JsonlIcon className='h-[14px] w-[14px]' />, label: 'JSONL' },
|
||||
created: { label: '3 days ago' },
|
||||
owner: ownerCell('J', 'Jordan P.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: {
|
||||
name: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'brand-guidelines.pdf' },
|
||||
size: { label: '5.1 MB' },
|
||||
type: { icon: <PdfIcon className='h-[14px] w-[14px]' />, label: 'PDF' },
|
||||
created: { label: '1 week ago' },
|
||||
owner: ownerCell('S', 'Sarah K.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: {
|
||||
name: { icon: <AudioIcon className='h-[14px] w-[14px]' />, label: 'customer-interviews.mp3' },
|
||||
size: { label: '45.2 MB' },
|
||||
type: { icon: <AudioIcon className='h-[14px] w-[14px]' />, label: 'Audio' },
|
||||
created: { label: 'March 20th, 2026' },
|
||||
owner: ownerCell('V', 'Vik M.'),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
cells: {
|
||||
name: { icon: <DocxIcon className='h-[14px] w-[14px]' />, label: 'onboarding-playbook.docx' },
|
||||
size: { label: '1.1 MB' },
|
||||
type: { icon: <DocxIcon className='h-[14px] w-[14px]' />, label: 'DOCX' },
|
||||
created: { label: 'March 14th, 2026' },
|
||||
owner: ownerCell('S', 'Sarah K.'),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Static landing preview of the Files workspace page.
|
||||
*/
|
||||
export function LandingPreviewFiles() {
|
||||
return (
|
||||
<LandingPreviewResource
|
||||
icon={File}
|
||||
title='Files'
|
||||
createLabel='Upload file'
|
||||
searchPlaceholder='Search files...'
|
||||
columns={COLUMNS}
|
||||
rows={ROWS}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { memo, useCallback, useRef, useState } from 'react'
|
||||
import { ArrowUp } from 'lucide-react'
|
||||
import { useLandingSubmit } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
|
||||
import { useAnimatedPlaceholder } from '@/app/workspace/[workspaceId]/home/hooks/use-animated-placeholder'
|
||||
import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder'
|
||||
|
||||
const C = {
|
||||
SURFACE: '#292929',
|
||||
@@ -48,17 +48,18 @@ export const LandingPreviewHome = memo(function LandingPreviewHome() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-[24px] pb-[2vh]'>
|
||||
<h1
|
||||
className='mb-[24px] max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
|
||||
<div className='flex min-w-0 flex-1 flex-col items-center justify-center px-6 pb-[2vh]'>
|
||||
<p
|
||||
role='presentation'
|
||||
className='mb-6 max-w-[42rem] font-[430] font-season text-[32px] tracking-[-0.02em]'
|
||||
style={{ color: C.TEXT_PRIMARY }}
|
||||
>
|
||||
What should we get done?
|
||||
</h1>
|
||||
</p>
|
||||
|
||||
<div className='w-full max-w-[32rem]'>
|
||||
<div
|
||||
className='cursor-text rounded-[20px] border px-[10px] py-[8px]'
|
||||
className='cursor-text rounded-[20px] border px-2.5 py-2'
|
||||
style={{ borderColor: C.BORDER, backgroundColor: C.SURFACE }}
|
||||
onClick={() => textareaRef.current?.focus()}
|
||||
>
|
||||
@@ -70,7 +71,7 @@ export const LandingPreviewHome = memo(function LandingPreviewHome() {
|
||||
onInput={handleInput}
|
||||
placeholder={animatedPlaceholder}
|
||||
rows={1}
|
||||
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-[4px] py-[4px] font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
|
||||
className='m-0 box-border min-h-[24px] w-full resize-none overflow-y-auto border-0 bg-transparent px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em] outline-none placeholder:font-[380] placeholder:text-[#787878] focus-visible:ring-0'
|
||||
style={{
|
||||
color: C.TEXT_PRIMARY,
|
||||
caretColor: C.TEXT_PRIMARY,
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Database } from '@/components/emcn/icons'
|
||||
import {
|
||||
AirtableIcon,
|
||||
AsanaIcon,
|
||||
ConfluenceIcon,
|
||||
GoogleDocsIcon,
|
||||
GoogleDriveIcon,
|
||||
JiraIcon,
|
||||
SalesforceIcon,
|
||||
SlackIcon,
|
||||
ZendeskIcon,
|
||||
} from '@/components/icons'
|
||||
import type {
|
||||
PreviewColumn,
|
||||
PreviewRow,
|
||||
} from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
import { LandingPreviewResource } from '@/app/(home)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
|
||||
|
||||
const DB_ICON = <Database className='h-[14px] w-[14px]' />
|
||||
|
||||
function connectorIcons(icons: React.ComponentType<{ className?: string }>[]) {
|
||||
return {
|
||||
content: (
|
||||
<div className='flex items-center gap-1'>
|
||||
{icons.map((Icon, i) => (
|
||||
<Icon key={i} className='h-3.5 w-3.5 flex-shrink-0' />
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const COLUMNS: PreviewColumn[] = [
|
||||
{ id: 'name', header: 'Name' },
|
||||
{ id: 'documents', header: 'Documents' },
|
||||
{ id: 'tokens', header: 'Tokens' },
|
||||
{ id: 'connectors', header: 'Connectors' },
|
||||
{ id: 'created', header: 'Created' },
|
||||
]
|
||||
|
||||
const ROWS: PreviewRow[] = [
|
||||
{
|
||||
id: '1',
|
||||
cells: {
|
||||
name: { icon: DB_ICON, label: 'Product Documentation' },
|
||||
documents: { label: '847' },
|
||||
tokens: { label: '1,284,392' },
|
||||
connectors: connectorIcons([AsanaIcon, GoogleDocsIcon]),
|
||||
created: { label: '2 days ago' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cells: {
|
||||
name: { icon: DB_ICON, label: 'Customer Support KB' },
|
||||
documents: { label: '234' },
|
||||
tokens: { label: '892,104' },
|
||||
connectors: connectorIcons([ZendeskIcon, SlackIcon]),
|
||||
created: { label: '1 week ago' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cells: {
|
||||
name: { icon: DB_ICON, label: 'Engineering Wiki' },
|
||||
documents: { label: '1,203' },
|
||||
tokens: { label: '2,847,293' },
|
||||
connectors: connectorIcons([ConfluenceIcon, JiraIcon]),
|
||||
created: { label: 'March 12th, 2026' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cells: {
|
||||
name: { icon: DB_ICON, label: 'Marketing Assets' },
|
||||
documents: { label: '189' },
|
||||
tokens: { label: '634,821' },
|
||||
connectors: connectorIcons([GoogleDriveIcon, AirtableIcon]),
|
||||
created: { label: 'March 5th, 2026' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
cells: {
|
||||
name: { icon: DB_ICON, label: 'Sales Playbook' },
|
||||
documents: { label: '92' },
|
||||
tokens: { label: '418,570' },
|
||||
connectors: connectorIcons([SalesforceIcon]),
|
||||
created: { label: 'February 28th, 2026' },
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export function LandingPreviewKnowledge() {
|
||||
return (
|
||||
<LandingPreviewResource
|
||||
icon={Database}
|
||||
title='Knowledge Base'
|
||||
createLabel='New base'
|
||||
searchPlaceholder='Search knowledge bases...'
|
||||
columns={COLUMNS}
|
||||
rows={ROWS}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Download } from 'lucide-react'
|
||||
import { ArrowUpDown, Badge, Library, ListFilter, Search } from '@/components/emcn'
|
||||
import type { BadgeProps } from '@/components/emcn/components/badge/badge'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
interface LogRow {
|
||||
id: string
|
||||
workflowName: string
|
||||
workflowColor: string
|
||||
date: string
|
||||
status: 'completed' | 'error' | 'running'
|
||||
cost: string
|
||||
trigger: 'webhook' | 'api' | 'schedule' | 'manual' | 'mcp' | 'chat'
|
||||
triggerLabel: string
|
||||
duration: string
|
||||
}
|
||||
|
||||
type BadgeVariant = BadgeProps['variant']
|
||||
|
||||
const STATUS_VARIANT: Record<LogRow['status'], BadgeVariant> = {
|
||||
completed: 'gray',
|
||||
error: 'red',
|
||||
running: 'amber',
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<LogRow['status'], string> = {
|
||||
completed: 'Completed',
|
||||
error: 'Error',
|
||||
running: 'Running',
|
||||
}
|
||||
|
||||
const TRIGGER_VARIANT: Record<LogRow['trigger'], BadgeVariant> = {
|
||||
webhook: 'orange',
|
||||
api: 'blue',
|
||||
schedule: 'green',
|
||||
manual: 'gray-secondary',
|
||||
mcp: 'cyan',
|
||||
chat: 'purple',
|
||||
}
|
||||
|
||||
const MOCK_LOGS: LogRow[] = [
|
||||
{
|
||||
id: '1',
|
||||
workflowName: 'Customer Onboarding',
|
||||
workflowColor: '#4f8ef7',
|
||||
date: 'Apr 1 10:42 AM',
|
||||
status: 'running',
|
||||
cost: '-',
|
||||
trigger: 'webhook',
|
||||
triggerLabel: 'Webhook',
|
||||
duration: '-',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
workflowName: 'Lead Enrichment',
|
||||
workflowColor: '#33C482',
|
||||
date: 'Apr 1 09:15 AM',
|
||||
status: 'error',
|
||||
cost: '318 credits',
|
||||
trigger: 'api',
|
||||
triggerLabel: 'API',
|
||||
duration: '2.7s',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
workflowName: 'Email Campaign',
|
||||
workflowColor: '#a855f7',
|
||||
date: 'Apr 1 08:30 AM',
|
||||
status: 'completed',
|
||||
cost: '89 credits',
|
||||
trigger: 'schedule',
|
||||
triggerLabel: 'Schedule',
|
||||
duration: '0.8s',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
workflowName: 'Data Pipeline',
|
||||
workflowColor: '#f97316',
|
||||
date: 'Mar 31 10:14 PM',
|
||||
status: 'completed',
|
||||
cost: '241 credits',
|
||||
trigger: 'webhook',
|
||||
triggerLabel: 'Webhook',
|
||||
duration: '4.1s',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
workflowName: 'Invoice Processing',
|
||||
workflowColor: '#ec4899',
|
||||
date: 'Mar 31 08:45 PM',
|
||||
status: 'completed',
|
||||
cost: '112 credits',
|
||||
trigger: 'manual',
|
||||
triggerLabel: 'Manual',
|
||||
duration: '0.9s',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
workflowName: 'Support Triage',
|
||||
workflowColor: '#0ea5e9',
|
||||
date: 'Mar 31 07:22 PM',
|
||||
status: 'completed',
|
||||
cost: '197 credits',
|
||||
trigger: 'api',
|
||||
triggerLabel: 'API',
|
||||
duration: '1.6s',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
workflowName: 'Content Moderator',
|
||||
workflowColor: '#f59e0b',
|
||||
date: 'Mar 31 06:11 PM',
|
||||
status: 'error',
|
||||
cost: '284 credits',
|
||||
trigger: 'schedule',
|
||||
triggerLabel: 'Schedule',
|
||||
duration: '3.2s',
|
||||
},
|
||||
]
|
||||
|
||||
type SortKey = 'workflowName' | 'date' | 'status' | 'cost' | 'trigger' | 'duration'
|
||||
|
||||
const COL_HEADERS: { key: SortKey; label: string }[] = [
|
||||
{ key: 'workflowName', label: 'Workflow' },
|
||||
{ key: 'date', label: 'Date' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'cost', label: 'Cost' },
|
||||
{ key: 'trigger', label: 'Trigger' },
|
||||
{ key: 'duration', label: 'Duration' },
|
||||
]
|
||||
|
||||
export function LandingPreviewLogs() {
|
||||
const [search, setSearch] = useState('')
|
||||
const [sortKey, setSortKey] = useState<SortKey | null>(null)
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
|
||||
const [activeTab, setActiveTab] = useState<'logs' | 'dashboard'>('logs')
|
||||
|
||||
function handleSort(key: SortKey) {
|
||||
if (sortKey === key) {
|
||||
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
|
||||
} else {
|
||||
setSortKey(key)
|
||||
setSortDir('asc')
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const q = search.toLowerCase()
|
||||
const filtered = q
|
||||
? MOCK_LOGS.filter(
|
||||
(log) =>
|
||||
log.workflowName.toLowerCase().includes(q) ||
|
||||
log.triggerLabel.toLowerCase().includes(q) ||
|
||||
STATUS_LABELS[log.status].toLowerCase().includes(q)
|
||||
)
|
||||
: MOCK_LOGS
|
||||
|
||||
if (!sortKey) return filtered
|
||||
return [...filtered].sort((a, b) => {
|
||||
const av = sortKey === 'cost' ? a.cost.replace(/\D/g, '') : a[sortKey]
|
||||
const bv = sortKey === 'cost' ? b.cost.replace(/\D/g, '') : b[sortKey]
|
||||
const cmp = av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' })
|
||||
return sortDir === 'asc' ? cmp : -cmp
|
||||
})
|
||||
}, [search, sortKey, sortDir])
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
{/* Header */}
|
||||
<div className='border-[var(--border)] border-b px-6 py-2.5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Library className='h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
<h1 className='font-medium text-[var(--text-body)] text-sm'>Logs</h1>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
|
||||
<Download className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
Export
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setActiveTab('logs')}
|
||||
className='rounded-md px-2 py-1 text-caption transition-colors'
|
||||
style={{
|
||||
backgroundColor: activeTab === 'logs' ? 'var(--surface-active)' : 'transparent',
|
||||
color: activeTab === 'logs' ? 'var(--text-body)' : 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
Logs
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
className='rounded-md px-2 py-1 text-caption transition-colors'
|
||||
style={{
|
||||
backgroundColor:
|
||||
activeTab === 'dashboard' ? 'var(--surface-active)' : 'transparent',
|
||||
color: activeTab === 'dashboard' ? 'var(--text-body)' : 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Options bar */}
|
||||
<div className='border-[var(--border)] border-b px-6 py-2.5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex flex-1 items-center gap-2.5'>
|
||||
<Search className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
<input
|
||||
type='text'
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder='Search logs...'
|
||||
className='flex-1 bg-transparent text-[var(--text-body)] text-caption outline-none placeholder:text-[var(--text-subtle)]'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<div className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
|
||||
<ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
Filter
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleSort(sortKey ?? 'workflowName')}
|
||||
className='flex cursor-default items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-3)]'
|
||||
>
|
||||
<ArrowUpDown className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
Sort
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table — uses <table> for pixel-perfect column alignment with headers */}
|
||||
<div className='min-h-0 flex-1 overflow-hidden'>
|
||||
<table className='w-full table-fixed text-sm'>
|
||||
<colgroup>
|
||||
<col style={{ width: '22%' }} />
|
||||
<col style={{ width: '18%' }} />
|
||||
<col style={{ width: '13%' }} />
|
||||
<col style={{ width: '15%' }} />
|
||||
<col style={{ width: '14%' }} />
|
||||
<col style={{ width: '18%' }} />
|
||||
</colgroup>
|
||||
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
|
||||
<tr>
|
||||
{COL_HEADERS.map(({ key, label }) => (
|
||||
<th
|
||||
key={key}
|
||||
className='h-10 px-6 py-1.5 text-left align-middle font-normal text-caption'
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleSort(key)}
|
||||
className={cn(
|
||||
'flex items-center gap-1 transition-colors hover-hover:text-[var(--text-secondary)]',
|
||||
sortKey === key ? 'text-[var(--text-secondary)]' : 'text-[var(--text-muted)]'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{sortKey === key && <ArrowUpDown className='h-[10px] w-[10px] opacity-60' />}
|
||||
</button>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sorted.map((log) => (
|
||||
<tr
|
||||
key={log.id}
|
||||
className='h-[44px] cursor-default transition-colors hover-hover:bg-[var(--surface-3)]'
|
||||
>
|
||||
<td className='px-6 align-middle'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div
|
||||
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px] border-[1.5px]'
|
||||
style={{
|
||||
backgroundColor: log.workflowColor,
|
||||
borderColor: `${log.workflowColor}60`,
|
||||
backgroundClip: 'padding-box',
|
||||
}}
|
||||
/>
|
||||
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-caption'>
|
||||
{log.workflowName}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
|
||||
{log.date}
|
||||
</td>
|
||||
<td className='px-6 align-middle'>
|
||||
<Badge variant={STATUS_VARIANT[log.status]} size='sm' dot>
|
||||
{STATUS_LABELS[log.status]}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
|
||||
{log.cost}
|
||||
</td>
|
||||
<td className='px-6 align-middle'>
|
||||
<Badge variant={TRIGGER_VARIANT[log.trigger]} size='sm'>
|
||||
{log.triggerLabel}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
|
||||
{log.duration}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||