Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06b1d82781 | ||
|
|
3d5d7474ed | ||
|
|
27794e59b3 | ||
|
|
88668fed84 | ||
|
|
fe5402a6d7 | ||
|
|
c436c2e378 | ||
|
|
60e905c520 | ||
|
|
1e55a0e044 | ||
|
|
e142753d64 | ||
|
|
61deb02959 | ||
|
|
e52862166d | ||
|
|
8f71684dcb | ||
|
|
92fe353f44 | ||
|
|
4c6c7272c5 | ||
|
|
55a9adfdda | ||
|
|
bdfe7e9b99 | ||
|
|
27c248a70c | ||
|
|
19ca9c78b4 | ||
|
|
b13f339327 | ||
|
|
aade4bf3ae | ||
|
|
b7185c9ee8 | ||
|
|
ca4b483ce3 | ||
|
|
adead6336b | ||
|
|
2d83bbf769 | ||
|
|
fd96e446ae | ||
|
|
8b095105ee | ||
|
|
4bb1237027 | ||
|
|
46f84e83e9 | ||
|
|
487f0328c9 | ||
|
|
b223e45de9 | ||
|
|
0c5e70fc23 | ||
|
|
bb759368d9 | ||
|
|
a7a2056b5f | ||
|
|
1213a64ecd | ||
|
|
39444afcdc | ||
|
|
a030329fd5 | ||
|
|
1420f4857b | ||
|
|
b1b8654236 | ||
|
|
d13a06e2c5 | ||
|
|
29d0732002 | ||
|
|
e8c51e99a2 |
@@ -1,7 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
@import "fumadocs-ui/css/neutral.css";
|
||||
@import "fumadocs-ui/css/preset.css";
|
||||
:root {
|
||||
|
||||
@theme {
|
||||
--color-fd-primary: #802fff; /* Purple from control-bar component */
|
||||
}
|
||||
|
||||
@@ -15,4 +16,9 @@
|
||||
color: var(--color-fd-primary);
|
||||
}
|
||||
|
||||
/* Tailwind v4 content sources */
|
||||
@source '../app/**/*.{js,ts,jsx,tsx,mdx}';
|
||||
@source '../components/**/*.{js,ts,jsx,tsx,mdx}';
|
||||
@source '../content/**/*.{js,ts,jsx,tsx,mdx}';
|
||||
@source '../mdx-components.tsx';
|
||||
@source '../node_modules/fumadocs-ui/dist/**/*.js';
|
||||
|
||||
@@ -270,3 +270,26 @@ export const ResponseIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<path d='m9 17-5-5 5-5' />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const StarterIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<path d='M8 5v14l11-7z' fill='currentColor' />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const LoopIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<path
|
||||
d='M4 12a8 8 0 018-8V2.5L16 6l-4 3.5V8a6 6 0 00-6 6 6 6 0 006 6 6 6 0 006-6h2a8 8 0 01-8 8 8 8 0 01-8-8z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const ParallelIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<rect x='3' y='3' width='18' height='6' rx='1' stroke='currentColor' strokeWidth='2' />
|
||||
<rect x='3' y='15' width='18' height='6' rx='1' stroke='currentColor' strokeWidth='2' />
|
||||
<path d='M12 9v6' stroke='currentColor' strokeWidth='2' strokeLinecap='round' />
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -8,47 +8,40 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Agent block is a fundamental component in Sim Studio that allows you to create powerful AI agents using various LLM providers. These agents can process inputs based on customizable system prompts and utilize integrated tools to enhance their capabilities.
|
||||
The Agent block serves as the interface between your workflow and Large Language Models (LLMs). It executes inference requests against various AI providers, processes natural language inputs according to defined instructions, and generates structured or unstructured outputs for downstream consumption.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/agent-light.png"
|
||||
darkSrc="/static/dark/agent-dark.png"
|
||||
alt="Agent Block"
|
||||
width={300}
|
||||
alt="Agent Block Configuration"
|
||||
width={350}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
<Callout type="info">
|
||||
Agent blocks serve as interfaces to Large Language Models, enabling your workflow to leverage
|
||||
state-of-the-art AI capabilities.
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
The Agent block serves as an interface to Large Language Models (LLMs), enabling you to create agents that can:
|
||||
The Agent block enables you to:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Respond to user inputs</strong>: Generate natural language responses based on provided
|
||||
inputs
|
||||
<strong>Process natural language</strong>: Analyze user input and generate contextual responses
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Follow instructions</strong>: Adhere to specific instructions defined in the system
|
||||
prompt
|
||||
<strong>Execute AI-powered tasks</strong>: Perform content analysis, generation, and decision-making
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Use specialized tools</strong>: Interact with integrated tools to extend capabilities
|
||||
<strong>Call external tools</strong>: Access APIs, databases, and services during processing
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Structure output</strong>: Generate responses in structured formats when needed
|
||||
<strong>Generate structured output</strong>: Return JSON data that matches your schema requirements
|
||||
</Step>
|
||||
</Steps>
|
||||
</Steps>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### System Prompt
|
||||
|
||||
The system prompt defines the agent's behavior, capabilities, and limitations. It's the primary way to instruct the agent on how to respond to inputs.
|
||||
The system prompt establishes the agent's operational parameters and behavioral constraints. This configuration defines the agent's role, response methodology, and processing boundaries for all incoming requests.
|
||||
|
||||
```markdown
|
||||
You are a helpful assistant that specializes in financial analysis.
|
||||
@@ -58,22 +51,25 @@ When responding to questions about investments, include risk disclaimers.
|
||||
|
||||
### User Prompt
|
||||
|
||||
The user prompt or context is the specific input or question that the agent should respond to. This can be:
|
||||
The user prompt represents the primary input data for inference processing. This parameter accepts natural language text or structured data that the agent will analyze and respond to. Input sources include:
|
||||
|
||||
- Directly provided in the block configuration
|
||||
- Connected from another block's output
|
||||
- Dynamically generated during workflow execution
|
||||
- **Static Configuration**: Direct text input specified in the block configuration
|
||||
- **Dynamic Input**: Data passed from upstream blocks through connection interfaces
|
||||
- **Runtime Generation**: Programmatically generated content during workflow execution
|
||||
|
||||
### Model Selection
|
||||
|
||||
Choose from a variety of LLM providers:
|
||||
The Agent block supports multiple LLM providers through a unified inference interface. Available models include:
|
||||
|
||||
- OpenAI (GPT-4o, o1, o3, o4-mini, gpt-4.1)
|
||||
- Anthropic (Claude 3.7 Sonnet)
|
||||
- Google (Gemini 2.5 Pro, Gemini 2.0 Flash)
|
||||
- Groq, Cerebras
|
||||
- Ollama Local Models
|
||||
- And more
|
||||
**OpenAI Models**: GPT-4o, o1, o3, o4-mini, gpt-4.1 (API-based inference)
|
||||
**Anthropic Models**: Claude 3.7 Sonnet (API-based inference)
|
||||
**Google Models**: Gemini 2.5 Pro, Gemini 2.0 Flash (API-based inference)
|
||||
**Alternative Providers**: Groq, Cerebras, xAI, DeepSeek (API-based inference)
|
||||
**Local Deployment**: Ollama-compatible models (self-hosted inference)
|
||||
|
||||
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/models.mp4"></video>
|
||||
</div>
|
||||
|
||||
### Temperature
|
||||
|
||||
@@ -81,22 +77,16 @@ Control the creativity and randomness of responses:
|
||||
|
||||
<Tabs items={['Low (0-0.3)', 'Medium (0.3-0.7)', 'High (0.7-2.0)']}>
|
||||
<Tab>
|
||||
<p>
|
||||
More deterministic, focused responses. Best for factual tasks, customer support, and
|
||||
situations where accuracy is critical.
|
||||
</p>
|
||||
More deterministic, focused responses. Best for factual tasks, customer support, and
|
||||
situations where accuracy is critical.
|
||||
</Tab>
|
||||
<Tab>
|
||||
<p>
|
||||
Balanced creativity and focus. Suitable for general purpose applications that require both
|
||||
accuracy and some creativity.
|
||||
</p>
|
||||
Balanced creativity and focus. Suitable for general purpose applications that require both
|
||||
accuracy and some creativity.
|
||||
</Tab>
|
||||
<Tab>
|
||||
<p>
|
||||
More creative, varied responses. Ideal for creative writing, brainstorming, and generating
|
||||
diverse ideas.
|
||||
</p>
|
||||
More creative, varied responses. Ideal for creative writing, brainstorming, and generating
|
||||
diverse ideas.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -110,103 +100,204 @@ Your API key for the selected LLM provider. This is securely stored and used for
|
||||
|
||||
### Tools
|
||||
|
||||
Integrate specialized tools to enhance the agent's capabilities. You can add tools to your agent by:
|
||||
Tools extend the agent's capabilities through external API integrations and service connections. The tool system enables function calling, allowing the agent to execute operations beyond text generation.
|
||||
|
||||
1. Clicking the Tools section in the Agent configuration
|
||||
2. Selecting from the tools dropdown menu
|
||||
3. Choosing an existing tool or creating a new one
|
||||
**Tool Integration Process**:
|
||||
1. Access the Tools configuration section within the Agent block
|
||||
2. Select from 60+ pre-built integrations or define custom functions
|
||||
3. Configure authentication parameters and operational constraints
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/tooldropdown-light.png"
|
||||
darkSrc="/static/dark/tooldropdown-dark.png"
|
||||
alt="Tools Dropdown"
|
||||
width={150}
|
||||
height={125}
|
||||
/>
|
||||
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/tools.mp4"></video>
|
||||
</div>
|
||||
|
||||
Available tools include:
|
||||
**Available Tool Categories**:
|
||||
- **Communication**: Gmail, Slack, Telegram, WhatsApp, Microsoft Teams
|
||||
- **Data Sources**: Notion, Google Sheets, Airtable, Supabase, Pinecone
|
||||
- **Web Services**: Firecrawl, Google Search, Exa AI, browser automation
|
||||
- **Development**: GitHub, Jira, Linear repository and issue management
|
||||
- **AI Services**: OpenAI, Perplexity, Hugging Face, ElevenLabs
|
||||
|
||||
- **Confluence**: Access and query Confluence knowledge bases
|
||||
- **Evaluator**: Use evaluation metrics to assess content
|
||||
- **GitHub**: Interact with GitHub repositories and issues
|
||||
- **Gmail**: Process and respond to emails
|
||||
- **Firecrawl**: Web search and content retrieval
|
||||
- And many, many more pre-built integrations
|
||||
**Tool Execution Control**:
|
||||
- **Auto**: Model determines tool invocation based on context and necessity
|
||||
- **Required**: Tool must be called during every inference request
|
||||
- **None**: Tool definition available but excluded from model context
|
||||
|
||||
You can also create custom tools to meet specific requirements for your agent's capabilities.
|
||||
|
||||
<Callout type="info">
|
||||
Tools significantly expand what your agent can do, allowing it to access external systems,
|
||||
retrieve information, and take actions beyond simple text generation.
|
||||
</Callout>
|
||||
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/granular-tool-control.mp4"></video>
|
||||
</div>
|
||||
|
||||
### Response Format
|
||||
|
||||
Define a structured format for the agent's response when needed, using JSON or other formats.
|
||||
The Response Format parameter enforces structured output generation through JSON Schema validation. This ensures consistent, machine-readable responses that conform to predefined data structures:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "user_analysis",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sentiment": {
|
||||
"type": "string",
|
||||
"enum": ["positive", "negative", "neutral"]
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
}
|
||||
},
|
||||
"required": ["sentiment", "confidence"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This configuration constrains the model's output to comply with the specified schema, preventing free-form text responses and ensuring structured data generation.
|
||||
|
||||
### Accessing Results
|
||||
|
||||
After an agent completes, you can access its outputs:
|
||||
|
||||
- **`<agent.content>`**: The agent's response text or structured data
|
||||
- **`<agent.tokens>`**: Token usage statistics (prompt, completion, total)
|
||||
- **`<agent.tool_calls>`**: Details of any tools the agent used during execution
|
||||
- **`<agent.cost>`**: Estimated cost of the API call (if available)
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Memory Integration
|
||||
|
||||
Agents can maintain context across interactions using the memory system:
|
||||
|
||||
```javascript
|
||||
// In a Function block before the agent
|
||||
const memory = {
|
||||
conversation_history: previousMessages,
|
||||
user_preferences: userProfile,
|
||||
session_data: currentSession
|
||||
};
|
||||
```
|
||||
|
||||
### Structured Output Validation
|
||||
|
||||
Use JSON Schema to ensure consistent, machine-readable responses:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"analysis": {"type": "string"},
|
||||
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
|
||||
"categories": {"type": "array", "items": {"type": "string"}}
|
||||
},
|
||||
"required": ["analysis", "confidence"]
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Agents automatically handle common errors:
|
||||
- API rate limits with exponential backoff
|
||||
- Invalid tool calls with retry logic
|
||||
- Network failures with connection recovery
|
||||
- Schema validation errors with fallback responses
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
<Tabs items={['Inputs', 'Outputs']}>
|
||||
<Tabs items={['Configuration', 'Variables', 'Results']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>User Prompt</strong>: The user's query or context for the agent
|
||||
<strong>System Prompt</strong>: Instructions defining agent behavior and role
|
||||
</li>
|
||||
<li>
|
||||
<strong>System Prompt</strong>: Instructions for the agent (optional)
|
||||
<strong>User Prompt</strong>: Input text or data to process
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tools</strong>: Optional tool connections that the agent can use
|
||||
<strong>Model</strong>: AI model selection (OpenAI, Anthropic, Google, etc.)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Temperature</strong>: Response randomness control (0-2)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tools</strong>: Array of available tools for function calling
|
||||
</li>
|
||||
<li>
|
||||
<strong>Response Format</strong>: JSON Schema for structured output
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Content</strong>: The agent's response text
|
||||
<strong>agent.content</strong>: Agent's response text or structured data
|
||||
</li>
|
||||
<li>
|
||||
<strong>Model</strong>: The model used for generation
|
||||
<strong>agent.tokens</strong>: Token usage statistics object
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tokens</strong>: Usage statistics (prompt, completion, total)
|
||||
<strong>agent.tool_calls</strong>: Array of tool execution details
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tool Calls</strong>: Details of any tools used during processing
|
||||
<strong>agent.cost</strong>: Estimated API call cost (if available)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Content</strong>: Primary response output from the agent
|
||||
</li>
|
||||
<li>
|
||||
<strong>Cost</strong>: Cost of the response
|
||||
<strong>Metadata</strong>: Usage statistics and execution details
|
||||
</li>
|
||||
<li>
|
||||
<strong>Usage</strong>: Usage statistics (prompt, completion, total)
|
||||
<strong>Access</strong>: Available in blocks after the agent
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Usage
|
||||
## Example Use Cases
|
||||
|
||||
Here's an example of how an Agent block might be configured for a customer support workflow:
|
||||
### Customer Support Automation
|
||||
|
||||
```yaml
|
||||
# Example Agent Configuration
|
||||
systemPrompt: |
|
||||
You are a customer support agent for TechCorp.
|
||||
Always maintain a professional, friendly tone.
|
||||
If you don't know an answer, direct the customer to email support@techcorp.com.
|
||||
Never make up information about products or policies.
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Handle customer inquiries with database access</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>User submits support ticket via API block</li>
|
||||
<li>Agent processes inquiry with product database tools</li>
|
||||
<li>Agent generates response and creates follow-up ticket</li>
|
||||
<li>Response block sends reply to customer</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
model: OpenAI/gpt-4
|
||||
temperature: 0.2
|
||||
tools:
|
||||
- ProductDatabase
|
||||
- OrderHistory
|
||||
- SupportTicketCreator
|
||||
```
|
||||
### Multi-Model Content Analysis
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Analyze content with different AI models</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Function block processes uploaded document</li>
|
||||
<li>Agent with GPT-4o performs technical analysis</li>
|
||||
<li>Agent with Claude analyzes sentiment and tone</li>
|
||||
<li>Function block combines results for final report</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Tool-Powered Research Assistant
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Research assistant with web search and document access</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>User query received via input</li>
|
||||
<li>Agent searches web using Google Search tool</li>
|
||||
<li>Agent accesses Notion database for internal docs</li>
|
||||
<li>Agent compiles comprehensive research report</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Be specific in system prompts**: Clearly define the agent's role, tone, and limitations. The more specific your instructions are, the better the agent will be able to fulfill its intended purpose.
|
||||
- **Choose the right temperature setting**: Use lower temperature settings (0-0.3) when accuracy is important, or increase temperature (0.7-2.0) for more creative or varied responses
|
||||
- **Combine with Evaluator blocks**: Use Evaluator blocks to assess agent responses and ensure quality. This allows you to create feedback loops and implement quality control measures.
|
||||
- **Leverage tools effectively**: Integrate tools that complement the agent's purpose and enhance its capabilities. Be selective about which tools you provide to avoid overwhelming the agent.
|
||||
- **Leverage tools effectively**: Integrate tools that complement the agent's purpose and enhance its capabilities. Be selective about which tools you provide to avoid overwhelming the agent. For tasks with little overlap, use another Agent block for the best results.
|
||||
|
||||
@@ -14,7 +14,7 @@ The API block enables you to connect your workflow to external services through
|
||||
lightSrc="/static/light/api-light.png"
|
||||
darkSrc="/static/dark/api-dark.png"
|
||||
alt="API Block"
|
||||
width={300}
|
||||
width={350}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
@@ -22,11 +22,20 @@ The API block enables you to connect your workflow to external services through
|
||||
|
||||
The API block enables you to:
|
||||
|
||||
- Make HTTP requests to external services and APIs
|
||||
- Process and transform data from external sources
|
||||
- Send data to external systems
|
||||
- Integrate with third-party platforms and services
|
||||
- Create webhooks and callbacks
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Connect to external services</strong>: Make HTTP requests to REST APIs and web services
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Send and receive data</strong>: Process responses and transform data from external sources
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Integrate third-party platforms</strong>: Connect with services like Stripe, Slack, or custom APIs
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Handle authentication</strong>: Support various auth methods including Bearer tokens and API keys
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
@@ -82,42 +91,140 @@ For methods that support a request body (POST, PUT, PATCH), you can define the d
|
||||
- Data connected from another block's output
|
||||
- Dynamically generated during workflow execution
|
||||
|
||||
### Accessing Results
|
||||
|
||||
After an API request completes, you can access its outputs:
|
||||
|
||||
- **`<api.data>`**: The response body data from the API
|
||||
- **`<api.status>`**: HTTP status code (200, 404, 500, etc.)
|
||||
- **`<api.headers>`**: Response headers from the server
|
||||
- **`<api.error>`**: Error details if the request failed
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Dynamic URL Construction
|
||||
|
||||
Build URLs dynamically using variables from previous blocks:
|
||||
|
||||
```javascript
|
||||
// In a Function block before the API
|
||||
const userId = <start.userId>;
|
||||
const apiUrl = `https://api.example.com/users/${userId}/profile`;
|
||||
```
|
||||
|
||||
### Request Retries
|
||||
|
||||
The API block automatically handles:
|
||||
- Network timeouts with exponential backoff
|
||||
- Rate limit responses (429 status codes)
|
||||
- Server errors (5xx status codes) with retry logic
|
||||
- Connection failures with reconnection attempts
|
||||
|
||||
### Response Validation
|
||||
|
||||
Validate API responses before processing:
|
||||
|
||||
```javascript
|
||||
// In a Function block after the API
|
||||
if (<api.status> === 200) {
|
||||
const data = <api.data>;
|
||||
// Process successful response
|
||||
} else {
|
||||
// Handle error response
|
||||
console.error(`API Error: ${<api.status>}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
### Inputs
|
||||
<Tabs items={['Configuration', 'Variables', 'Results']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>URL</strong>: The endpoint to send the request to
|
||||
</li>
|
||||
<li>
|
||||
<strong>Method</strong>: HTTP method (GET, POST, PUT, DELETE, PATCH)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Query Parameters</strong>: Key-value pairs for URL parameters
|
||||
</li>
|
||||
<li>
|
||||
<strong>Headers</strong>: HTTP headers for authentication and content type
|
||||
</li>
|
||||
<li>
|
||||
<strong>Body</strong>: Request payload for POST/PUT/PATCH methods
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>api.data</strong>: Response body data from the API call
|
||||
</li>
|
||||
<li>
|
||||
<strong>api.status</strong>: HTTP status code returned by server
|
||||
</li>
|
||||
<li>
|
||||
<strong>api.headers</strong>: Response headers from the server
|
||||
</li>
|
||||
<li>
|
||||
<strong>api.error</strong>: Error details if request failed
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Response Data</strong>: Primary API response content
|
||||
</li>
|
||||
<li>
|
||||
<strong>Status Information</strong>: HTTP status and error details
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the API call
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
- **URL**: The endpoint to send the request to
|
||||
- **Method**: The HTTP method to use
|
||||
- **Query Parameters**: Key-value pairs for URL parameters
|
||||
- **Headers**: HTTP headers for the request
|
||||
- **Body**: Data to send with the request (for applicable methods)
|
||||
## Example Use Cases
|
||||
|
||||
### Outputs
|
||||
### Fetch User Profile Data
|
||||
|
||||
- **Status Code**: The HTTP status code returned by the server
|
||||
- **Response Body**: The data returned by the server
|
||||
- **Headers**: Response headers from the server
|
||||
- **Error**: Any error information if the request fails
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Retrieve user information from external service</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Function block constructs user ID from input</li>
|
||||
<li>API block calls GET /users/{id} endpoint</li>
|
||||
<li>Function block processes and formats user data</li>
|
||||
<li>Response block returns formatted profile</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
## Example Usage
|
||||
### Create Support Ticket
|
||||
|
||||
Here's an example of how an API block might be configured to fetch weather data:
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Submit support request to ticketing system</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Agent analyzes user issue and generates ticket data</li>
|
||||
<li>API block POSTs ticket to support system</li>
|
||||
<li>Condition block checks if ticket was created successfully</li>
|
||||
<li>Response block confirms ticket creation with ID</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
```yaml
|
||||
# Example API Configuration
|
||||
url: https://api.weatherapi.com/v1/current.json
|
||||
method: GET
|
||||
params:
|
||||
- key: key
|
||||
value: your_api_key_here
|
||||
- key: q
|
||||
value: London
|
||||
- key: aqi
|
||||
value: no
|
||||
headers:
|
||||
- key: Accept
|
||||
value: application/json
|
||||
```
|
||||
### Payment Processing
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Process payment through Stripe API</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Function block validates payment data</li>
|
||||
<li>API block creates payment intent via Stripe</li>
|
||||
<li>Condition block handles payment success/failure</li>
|
||||
<li>Function block updates order status in database</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -125,4 +232,3 @@ headers:
|
||||
- **Handle errors gracefully**: Connect error handling logic for failed requests
|
||||
- **Validate responses**: Check status codes and response formats before processing data
|
||||
- **Respect rate limits**: Be mindful of API rate limits and implement appropriate throttling
|
||||
- **Cache responses when appropriate**: For frequently accessed data that doesn't change often
|
||||
|
||||
@@ -4,9 +4,9 @@ description: Create conditional logic and branching in your workflows
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { File, Files, Folder } from 'fumadocs-ui/components/files'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Condition block allows you to branch your workflow execution path based on boolean expressions. It evaluates conditions and routes the workflow accordingly, enabling you to create dynamic, responsive workflows with different execution paths.
|
||||
@@ -15,7 +15,7 @@ The Condition block allows you to branch your workflow execution path based on b
|
||||
lightSrc="/static/light/condition-light.png"
|
||||
darkSrc="/static/dark/condition-dark.png"
|
||||
alt="Condition Block"
|
||||
width={300}
|
||||
width={350}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
@@ -26,57 +26,32 @@ The Condition block allows you to branch your workflow execution path based on b
|
||||
|
||||
## Overview
|
||||
|
||||
The Condition block serves as a decision point in your workflow, enabling:
|
||||
|
||||
<div className="my-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<h3 className="mb-2 text-lg font-medium">Branching Logic</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Create different execution paths based on specific conditions
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<h3 className="mb-2 text-lg font-medium">Rule-Based Routing</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Route workflows deterministically without needing an LLM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<h3 className="mb-2 text-lg font-medium">Data-Driven Decisions</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Create workflow paths based on structured data values
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<h3 className="mb-2 text-lg font-medium">If-Then-Else Logic</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Implement conditional programming paradigms in your workflows
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## How It Works
|
||||
|
||||
The Condition block:
|
||||
The Condition block enables you to:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Evaluate Expression</strong>: Evaluates a boolean expression or condition
|
||||
<strong>Create branching logic</strong>: Route workflows based on boolean expressions
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Determine Result</strong>: Determines whether the condition evaluates to true or false
|
||||
<strong>Make data-driven decisions</strong>: Evaluate conditions using previous block outputs
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Route Workflow</strong>: Routes the workflow to the appropriate path based on the result
|
||||
<strong>Handle multiple scenarios</strong>: Define multiple conditions with different paths
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Provide Context</strong>: Provides context about the decision made
|
||||
<strong>Provide deterministic routing</strong>: Make decisions without requiring an LLM
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## How It Works
|
||||
|
||||
The Condition block operates through a sequential evaluation process:
|
||||
|
||||
1. **Evaluate Expression** - Processes the JavaScript/TypeScript boolean expression using current workflow data
|
||||
2. **Determine Result** - Returns true or false based on the expression evaluation
|
||||
3. **Route Workflow** - Directs execution to the appropriate destination block based on the result
|
||||
4. **Provide Context** - Generates metadata about the decision for debugging and monitoring
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Conditions
|
||||
@@ -97,95 +72,165 @@ Conditions use JavaScript syntax and can reference input values from previous bl
|
||||
<Tab>
|
||||
```javascript
|
||||
// Check if a score is above a threshold
|
||||
input.score > 75
|
||||
<agent.score> > 75
|
||||
```
|
||||
</Tab>
|
||||
<Tab>
|
||||
```javascript
|
||||
// Check if a text contains specific keywords
|
||||
input.text.includes('urgent') || input.text.includes('emergency')
|
||||
<agent.text>.includes('urgent') || <agent.text>.includes('emergency')
|
||||
```
|
||||
</Tab>
|
||||
<Tab>
|
||||
```javascript
|
||||
// Check multiple conditions
|
||||
input.age >= 18 && input.country === 'US'
|
||||
<agent.age> >= 18 && <agent.country> === 'US'
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
### Accessing Results
|
||||
|
||||
After a condition evaluates, you can access its outputs:
|
||||
|
||||
- **`<condition.result>`**: Boolean result of the condition evaluation
|
||||
- **`<condition.matched_condition>`**: ID of the condition that was matched
|
||||
- **`<condition.content>`**: Description of the evaluation result
|
||||
- **`<condition.path>`**: Details of the chosen routing destination
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Complex Expressions
|
||||
|
||||
Use JavaScript operators and functions in conditions:
|
||||
|
||||
```javascript
|
||||
// String operations
|
||||
<user.email>.endsWith('@company.com')
|
||||
|
||||
// Array operations
|
||||
<api.tags>.includes('urgent')
|
||||
|
||||
// Mathematical operations
|
||||
<agent.confidence> * 100 > 85
|
||||
|
||||
// Date comparisons
|
||||
new Date(<api.created_at>) > new Date('2024-01-01')
|
||||
```
|
||||
|
||||
### Multiple Condition Evaluation
|
||||
|
||||
Conditions are evaluated in order until one matches:
|
||||
|
||||
```javascript
|
||||
// Condition 1: Check for high priority
|
||||
<ticket.priority> === 'high'
|
||||
|
||||
// Condition 2: Check for urgent keywords
|
||||
<ticket.subject>.toLowerCase().includes('urgent')
|
||||
|
||||
// Condition 3: Default fallback
|
||||
true
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Conditions automatically handle:
|
||||
- Undefined or null values with safe evaluation
|
||||
- Type mismatches with appropriate fallbacks
|
||||
- Invalid expressions with error logging
|
||||
- Missing variables with default values
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
<Tabs items={['Inputs', 'Outputs']}>
|
||||
<Tabs items={['Configuration', 'Variables', 'Results']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Variables</strong>: Values from previous blocks that can be referenced in conditions
|
||||
<strong>Conditions</strong>: Array of boolean expressions to evaluate
|
||||
</li>
|
||||
<li>
|
||||
<strong>Conditions</strong>: Boolean expressions to evaluate
|
||||
<strong>Expressions</strong>: JavaScript/TypeScript conditions using block outputs
|
||||
</li>
|
||||
<li>
|
||||
<strong>Routing Paths</strong>: Destination blocks for each condition result
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Content</strong>: A description of the evaluation result
|
||||
<strong>condition.result</strong>: Boolean result of condition evaluation
|
||||
</li>
|
||||
<li>
|
||||
<strong>Condition Result</strong>: The boolean result of the condition evaluation
|
||||
<strong>condition.matched_condition</strong>: ID of the matched condition
|
||||
</li>
|
||||
<li>
|
||||
<strong>Selected Path</strong>: Details of the chosen routing destination
|
||||
<strong>condition.content</strong>: Description of evaluation result
|
||||
</li>
|
||||
<li>
|
||||
<strong>Selected Condition ID</strong>: Identifier of the condition that was matched
|
||||
<strong>condition.path</strong>: Details of chosen routing destination
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Boolean Result</strong>: Primary condition evaluation outcome
|
||||
</li>
|
||||
<li>
|
||||
<strong>Routing Information</strong>: Path selection and condition details
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the condition
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Usage
|
||||
## Example Use Cases
|
||||
|
||||
Here's an example of how a Condition block might be used in a customer satisfaction workflow:
|
||||
### Customer Support Routing
|
||||
|
||||
```yaml
|
||||
# Example Condition Configuration
|
||||
conditions:
|
||||
- id: 'high_satisfaction'
|
||||
expression: 'input.satisfactionScore >= 8'
|
||||
description: 'Customer is highly satisfied'
|
||||
path: 'positive_feedback_block'
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Route support tickets based on priority</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>API block fetches support ticket data</li>
|
||||
<li>Condition checks if `<api.priority>` equals 'high'</li>
|
||||
<li>High priority tickets → Agent with escalation tools</li>
|
||||
<li>Normal priority tickets → Standard support agent</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
- id: 'medium_satisfaction'
|
||||
expression: 'input.satisfactionScore >= 5'
|
||||
description: 'Customer is moderately satisfied'
|
||||
path: 'neutral_feedback_block'
|
||||
### Content Moderation
|
||||
|
||||
- id: 'default'
|
||||
expression: 'true'
|
||||
description: 'Customer is not satisfied'
|
||||
path: 'improvement_feedback_block'
|
||||
```
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Filter content based on analysis results</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Agent analyzes user-generated content</li>
|
||||
<li>Condition checks if `<agent.toxicity_score>` > 0.7</li>
|
||||
<li>Toxic content → Moderation workflow</li>
|
||||
<li>Clean content → Publishing workflow</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### User Onboarding Flow
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Personalize onboarding based on user type</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Function block processes user registration data</li>
|
||||
<li>Condition checks if `<user.account_type>` === 'enterprise'</li>
|
||||
<li>Enterprise users → Advanced setup workflow</li>
|
||||
<li>Individual users → Simple onboarding workflow</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Order conditions correctly
|
||||
|
||||
Conditions are evaluated in order, so place more specific conditions before general ones. This ensures that more specific logic takes precedence over general fallbacks.
|
||||
|
||||
### Include a default condition
|
||||
|
||||
Add a catch-all condition (e.g., `true`) as the last condition to handle cases when no other conditions match. This prevents workflow execution from getting stuck.
|
||||
|
||||
### Keep expressions simple
|
||||
|
||||
Use clear, straightforward boolean expressions for better readability. Complex expressions can be difficult to debug and maintain.
|
||||
|
||||
### Document your conditions
|
||||
|
||||
Add descriptions to explain the purpose of each condition. This helps other team members understand the logic and makes maintenance easier.
|
||||
|
||||
### Test edge cases
|
||||
|
||||
Ensure your conditions handle boundary values correctly. Test with values at the edges of your condition ranges to verify correct behavior.
|
||||
- **Order conditions correctly**: Place more specific conditions before general ones to ensure specific logic takes precedence over fallbacks
|
||||
- **Include a default condition**: Add a catch-all condition (`true`) as the last condition to handle unmatched cases and prevent workflow execution from getting stuck
|
||||
- **Keep expressions simple**: Use clear, straightforward boolean expressions for better readability and easier debugging
|
||||
- **Document your conditions**: Add descriptions to explain the purpose of each condition for better team collaboration and maintenance
|
||||
- **Test edge cases**: Verify conditions handle boundary values correctly by testing with values at the edges of your condition ranges
|
||||
|
||||
@@ -8,24 +8,23 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Evaluator block allows you to assess the quality of content using customizable evaluation metrics. This is particularly useful for evaluating AI-generated text, ensuring outputs meet specific criteria, and building quality-control mechanisms into your workflows.
|
||||
The Evaluator block uses AI to score and assess content quality based on metrics you define. Perfect for quality control, A/B testing, and ensuring your AI outputs meet specific standards.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/evaluator-light.png"
|
||||
darkSrc="/static/dark/evaluator-dark.png"
|
||||
alt="Evaluator Block"
|
||||
width={300}
|
||||
alt="Evaluator Block Configuration"
|
||||
width={350}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
## Overview
|
||||
## What You Can Evaluate
|
||||
|
||||
The Evaluator block utilizes LLMs to objectively evaluate content based on custom metrics you define. This is especially useful for:
|
||||
|
||||
- Assessing the quality of AI-generated content
|
||||
- Evaluating responses against specific criteria
|
||||
- Creating scoring frameworks for different types of content
|
||||
- Building objective feedback loops in your workflows
|
||||
**AI-Generated Content**: Score chatbot responses, generated articles, or marketing copy
|
||||
**User Input**: Evaluate customer feedback, survey responses, or form submissions
|
||||
**Content Quality**: Assess clarity, accuracy, relevance, and tone
|
||||
**Performance Metrics**: Track improvements over time with consistent scoring
|
||||
**A/B Testing**: Compare different approaches with objective metrics
|
||||
|
||||
## Configuration Options
|
||||
|
||||
@@ -55,16 +54,19 @@ The content to be evaluated. This can be:
|
||||
|
||||
### Model Selection
|
||||
|
||||
Choose an LLM provider to perform the evaluation:
|
||||
Choose an AI model to perform the evaluation:
|
||||
|
||||
- OpenAI (GPT-4o, o1, o3, , gpt-4.1)
|
||||
- Anthropic (Claude 3.7 Sonnet)
|
||||
- Google (Gemini 2.5 Pro, Gemini 2.0 Flash)
|
||||
- Groq, Cerebras
|
||||
- Ollama Local Models
|
||||
- And more
|
||||
**OpenAI**: GPT-4o, o1, o3, o4-mini, gpt-4.1
|
||||
**Anthropic**: Claude 3.7 Sonnet
|
||||
**Google**: Gemini 2.5 Pro, Gemini 2.0 Flash
|
||||
**Other Providers**: Groq, Cerebras, xAI, DeepSeek
|
||||
**Local Models**: Any model running on Ollama
|
||||
|
||||
The chosen model should have strong reasoning capabilities to provide accurate evaluations.
|
||||
<div className="w-full max-w-2xl mx-auto overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/models.mp4"></video>
|
||||
</div>
|
||||
|
||||
**Recommendation**: Use models with strong reasoning capabilities like GPT-4o or Claude 3.7 Sonnet for more accurate evaluations.
|
||||
|
||||
### API Key
|
||||
|
||||
|
||||
@@ -8,99 +8,281 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Function block allows you to write and execute custom JavaScript or TypeScript code directly within your workflow. This powerful feature enables you to implement complex logic, data transformations, and integration with external libraries.
|
||||
The Function block lets you run custom JavaScript or TypeScript code in your workflow. Use it to transform data, perform calculations, or implement custom logic that isn't available in other blocks.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/function-light.png"
|
||||
darkSrc="/static/dark/function-dark.png"
|
||||
alt="Function Block"
|
||||
width={300}
|
||||
alt="Function Block with Code Editor"
|
||||
width={350}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
## Overview
|
||||
|
||||
The Function block brings the full power of JavaScript/TypeScript to your workflows, allowing for:
|
||||
The Function block enables you to:
|
||||
|
||||
- Custom data transformation and manipulation
|
||||
- Complex conditional logic
|
||||
- Mathematical calculations and algorithms
|
||||
- Integration with external libraries
|
||||
- Creation of reusable utility functions
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Transform data</strong>: Convert formats, parse text, manipulate arrays and objects
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Perform calculations</strong>: Math operations, statistics, financial calculations
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Implement custom logic</strong>: Complex conditionals, loops, and algorithms
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Process external data</strong>: Parse responses, format requests, handle authentication
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## How It Works
|
||||
|
||||
The Function block:
|
||||
The Function block runs your code in a secure, isolated environment:
|
||||
|
||||
1. Takes your custom JavaScript/TypeScript code
|
||||
2. Executes it in a secure, isolated environment
|
||||
3. Processes any inputs provided from previous blocks
|
||||
4. Returns the result for use in subsequent blocks
|
||||
1. **Receive Input**: Access data from previous blocks via the `input` object
|
||||
2. **Execute Code**: Run your JavaScript/TypeScript code
|
||||
3. **Return Results**: Use `return` to pass data to the next block
|
||||
4. **Handle Errors**: Built-in error handling and logging
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Code Editor
|
||||
|
||||
A full-featured code editor where you can write your JavaScript/TypeScript code. The editor supports:
|
||||
Write your JavaScript/TypeScript code in a full-featured editor with:
|
||||
- Syntax highlighting and error checking
|
||||
- Line numbers and bracket matching
|
||||
- Support for modern JavaScript features
|
||||
- Native support for `fetch`
|
||||
|
||||
- Syntax highlighting
|
||||
- Code completion
|
||||
- Error checking
|
||||
- Multiple lines of code
|
||||
### Accessing Input Data
|
||||
|
||||
### Inputs
|
||||
|
||||
Your function can access inputs from previous blocks through an `input` object. For example:
|
||||
Use the `input` object to access data from previous blocks:
|
||||
|
||||
```javascript
|
||||
// Access data from a previous block
|
||||
const customerName = input.customerData.name;
|
||||
const orderTotal = input.orderData.total;
|
||||
// Access data from connected blocks
|
||||
const userData = <agent.userData>;
|
||||
const orderData = <agent.orderData>;
|
||||
|
||||
// Process the data
|
||||
const discount = orderTotal > 100 ? 0.1 : 0;
|
||||
const finalPrice = orderTotal * (1 - discount);
|
||||
|
||||
// Return the result
|
||||
return {
|
||||
customerName,
|
||||
originalTotal: orderTotal,
|
||||
discount: discount * 100 + '%',
|
||||
finalPrice
|
||||
};
|
||||
// Access specific fields
|
||||
const customerName = <agent.customer.name>;
|
||||
const total = <agent.order.total>;
|
||||
```
|
||||
|
||||
## Safety and Limitations
|
||||
### Common Examples
|
||||
|
||||
For security and performance reasons, function execution has certain limitations:
|
||||
**Data Transformation**:
|
||||
```javascript
|
||||
// Convert and format data
|
||||
const formatted = {
|
||||
name: <agent.user.firstName> + ' ' + <agent.user.lastName>,
|
||||
email: <agent.user.email>.toLowerCase(),
|
||||
joinDate: new Date(<agent.user.created>).toLocaleDateString()
|
||||
};
|
||||
return formatted;
|
||||
```
|
||||
|
||||
- **Execution Time**: Functions have a maximum execution time to prevent infinite loops
|
||||
- **Memory Usage**: Memory is limited to prevent excessive resource usage
|
||||
- **Network Access**: Network calls are restricted to prevent unauthorized access
|
||||
- **Available APIs**: Only a subset of browser APIs are available
|
||||
**Calculations**:
|
||||
```javascript
|
||||
// Calculate discounts and totals
|
||||
const subtotal = <agent.items>.reduce((sum, item) => sum + item.price, 0);
|
||||
const discount = subtotal > 100 ? 0.1 : 0;
|
||||
const total = subtotal * (1 - discount);
|
||||
|
||||
return { subtotal, discount, total };
|
||||
```
|
||||
|
||||
**Data Validation**:
|
||||
```javascript
|
||||
// Validate email format
|
||||
const email = <agent.email>;
|
||||
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid email format');
|
||||
}
|
||||
return { email, isValid };
|
||||
```
|
||||
|
||||
### Accessing Results
|
||||
|
||||
After a function executes, you can access its outputs:
|
||||
|
||||
- **`<function.result>`**: The value returned from your function
|
||||
- **`<function.stdout>`**: Any console.log() output from your code
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Async/Await Support
|
||||
|
||||
Use async functions for complex operations:
|
||||
|
||||
```javascript
|
||||
// Async function example
|
||||
const processData = async () => {
|
||||
const data = <api.response>;
|
||||
|
||||
// Process data with async operations
|
||||
const processed = await Promise.all(
|
||||
data.map(async (item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
processed: true,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return processed;
|
||||
};
|
||||
|
||||
return await processData();
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Implement robust error handling:
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const result = <api.data>;
|
||||
|
||||
if (!result || !result.length) {
|
||||
throw new Error('No data received');
|
||||
}
|
||||
|
||||
return result.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name.trim(),
|
||||
valid: true
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Processing failed:', error.message);
|
||||
return { error: error.message, valid: false };
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
Optimize for large datasets:
|
||||
|
||||
```javascript
|
||||
// Efficient data processing
|
||||
const data = <api.large_dataset>;
|
||||
|
||||
// Use efficient array methods
|
||||
const processed = data
|
||||
.filter(item => item.status === 'active')
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
summary: item.description.substring(0, 100)
|
||||
}))
|
||||
.slice(0, 1000); // Limit results
|
||||
|
||||
return processed;
|
||||
```
|
||||
|
||||
## Security and Limitations
|
||||
|
||||
<Callout type="warning">
|
||||
Functions run in a secure environment with these restrictions:
|
||||
- **Execution timeout**: 30 seconds maximum to prevent infinite loops
|
||||
- **Memory limits**: Limited memory to prevent resource exhaustion
|
||||
- **No network access**: Cannot make HTTP requests (use API blocks instead)
|
||||
- **Limited APIs**: Only safe JavaScript APIs are available
|
||||
</Callout>
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
### Inputs
|
||||
<Tabs items={['Configuration', 'Variables', 'Results']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Code</strong>: Your JavaScript/TypeScript code to execute
|
||||
</li>
|
||||
<li>
|
||||
<strong>Timeout</strong>: Maximum execution time (defaults to 30 seconds)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Input Data</strong>: All connected block outputs available via variables
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>function.result</strong>: The value returned from your function
|
||||
</li>
|
||||
<li>
|
||||
<strong>function.stdout</strong>: Console.log() output from your code
|
||||
</li>
|
||||
<li>
|
||||
<strong>function.error</strong>: Error details if function failed
|
||||
</li>
|
||||
<li>
|
||||
<strong>function.execution_time</strong>: Time taken to execute
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Function Result</strong>: Primary output from your code
|
||||
</li>
|
||||
<li>
|
||||
<strong>Debug Information</strong>: Logs and execution details
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the function
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
- **Code**: Your JavaScript/TypeScript code to execute
|
||||
- **Input Data**: Values from previous blocks that can be accessed in your code
|
||||
## Example Use Cases
|
||||
|
||||
### Outputs
|
||||
### Data Processing Pipeline
|
||||
|
||||
- **result**: The value returned by your function
|
||||
- **stdout**: Any console output from your function
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Transform API response into structured data</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>API block fetches raw customer data</li>
|
||||
<li>Function block processes and validates data</li>
|
||||
<li>Function block calculates derived metrics</li>
|
||||
<li>Response block returns formatted results</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
## Example Usage
|
||||
### Business Logic Implementation
|
||||
|
||||
Here's an example of a Function block that processes customer data and calculates a loyalty score:
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Calculate loyalty scores and tiers</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Agent retrieves customer purchase history</li>
|
||||
<li>Function block calculates loyalty metrics</li>
|
||||
<li>Function block determines customer tier</li>
|
||||
<li>Condition block routes based on tier level</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
```javascript
|
||||
// Example Function Block Code
|
||||
### Data Validation and Sanitization
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Validate and clean user input</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>User input received from form submission</li>
|
||||
<li>Function block validates email format and phone numbers</li>
|
||||
<li>Function block sanitizes and normalizes data</li>
|
||||
<li>API block saves validated data to database</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Example: Loyalty Score Calculator
|
||||
|
||||
```javascript title="loyalty-calculator.js"
|
||||
// Process customer data and calculate loyalty score
|
||||
|
||||
// Access input from previous blocks
|
||||
const { purchaseHistory, accountAge, supportTickets } = input;
|
||||
const { purchaseHistory, accountAge, supportTickets } = <agent>;
|
||||
|
||||
// Calculate metrics
|
||||
const totalSpent = purchaseHistory.reduce((sum, purchase) => sum + purchase.amount, 0);
|
||||
@@ -114,23 +296,18 @@ const supportScore = ticketRatio * 30;
|
||||
|
||||
const loyaltyScore = Math.round(spendScore + frequencyScore + supportScore);
|
||||
|
||||
// Return results
|
||||
return {
|
||||
customer: input.name,
|
||||
customer: <agent.name>,
|
||||
loyaltyScore,
|
||||
loyaltyTier: loyaltyScore >= 80 ? "Platinum" : loyaltyScore >= 60 ? "Gold" : loyaltyScore >= 40 ? "Silver" : "Bronze",
|
||||
metrics: {
|
||||
spendScore,
|
||||
frequencyScore,
|
||||
supportScore
|
||||
}
|
||||
loyaltyTier: loyaltyScore >= 80 ? "Platinum" : loyaltyScore >= 60 ? "Gold" : "Silver",
|
||||
metrics: { spendScore, frequencyScore, supportScore }
|
||||
};
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Keep functions focused**: Write functions that do one thing well
|
||||
- **Handle errors gracefully**: Use try/catch blocks to handle potential errors
|
||||
- **Document your code**: Add comments to explain complex logic
|
||||
- **Test edge cases**: Ensure your code handles unusual inputs correctly
|
||||
- **Optimize for performance**: Be mindful of computational complexity for large datasets
|
||||
- **Keep functions focused**: Write functions that do one thing well to improve maintainability and debugging
|
||||
- **Handle errors gracefully**: Use try/catch blocks to handle potential errors and provide meaningful error messages
|
||||
- **Test edge cases**: Ensure your code handles unusual inputs, null values, and boundary conditions correctly
|
||||
- **Optimize for performance**: Be mindful of computational complexity and memory usage for large datasets
|
||||
- **Use console.log() for debugging**: Leverage stdout output to debug and monitor function execution
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Blocks
|
||||
description: Building blocks for your agentic workflows
|
||||
description: The building components of your AI workflows
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
@@ -8,47 +8,121 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { BlockTypes } from '@/components/ui/block-types'
|
||||
|
||||
Blocks are the fundamental building components of Sim Studio workflows. Each block has a specific purpose and can be connected to other blocks to create sophisticated workflows.
|
||||
Blocks are the building components you connect together to create AI workflows. Think of them as specialized modules that each handle a specific task—from chatting with AI models to making API calls or processing data.
|
||||
|
||||
## What is a Block?
|
||||
<div className="w-full max-w-2xl mx-auto overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/connections.mp4"></video>
|
||||
</div>
|
||||
|
||||
A block is a reusable, configurable component that performs a specific function within your workflow. Blocks have inputs and outputs that allow them to communicate with other blocks. They can process data, make decisions, interact with external systems, or perform computations.
|
||||
## Core Block Types
|
||||
|
||||
## Primary Block Types
|
||||
Sim Studio provides seven core block types that handle the essential functions of AI workflows:
|
||||
|
||||
Sim Studio provides six powerful block types that form the foundation of any workflow. Each block is designed to handle specific aspects of your agentic applications, from AI-powered reasoning to conditional logic and external integrations.
|
||||
### Processing Blocks
|
||||
- **[Agent](/blocks/agent)** - Chat with AI models (OpenAI, Anthropic, Google, local models)
|
||||
- **[Function](/blocks/function)** - Run custom JavaScript/TypeScript code
|
||||
- **[API](/blocks/api)** - Connect to external services via HTTP requests
|
||||
|
||||
### Logic Blocks
|
||||
- **[Condition](/blocks/condition)** - Branch workflow paths based on boolean expressions
|
||||
- **[Router](/blocks/router)** - Use AI to intelligently route requests to different paths
|
||||
- **[Evaluator](/blocks/evaluator)** - Score and assess content quality using AI
|
||||
|
||||
### Output Blocks
|
||||
- **[Response](/blocks/response)** - Format and return final results from your workflow
|
||||
|
||||
<BlockTypes />
|
||||
|
||||
## Block Connections
|
||||
## How Blocks Work
|
||||
|
||||
Blocks can be connected to form a directed graph representing your workflow. Each connection represents the flow of data from one block to another:
|
||||
Each block has three main components:
|
||||
|
||||
**Inputs**: Data coming into the block from other blocks or user input
|
||||
**Configuration**: Settings that control how the block behaves
|
||||
**Outputs**: Data the block produces for other blocks to use
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Outputs to Inputs</strong>: A block's outputs can be connected to another block's
|
||||
inputs.
|
||||
<strong>Receive Input</strong>: Block receives data from connected blocks or user input
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Multiple Connections</strong>: A block can have multiple incoming and outgoing
|
||||
connections.
|
||||
<strong>Process</strong>: Block processes the input according to its configuration
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Conditional Flows</strong>: Some blocks (like Router and Condition) can have multiple
|
||||
output paths based on conditions.
|
||||
<strong>Output Results</strong>: Block produces output data for the next blocks in the workflow
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Connecting Blocks
|
||||
|
||||
You create workflows by connecting blocks together. The output of one block becomes the input of another:
|
||||
|
||||
- **Drag to connect**: Drag from an output port to an input port
|
||||
- **Multiple connections**: One output can connect to multiple inputs
|
||||
- **Branching paths**: Some blocks can route to different paths based on conditions
|
||||
|
||||
<div className="w-full max-w-2xl mx-auto overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/connections.mp4"></video>
|
||||
</div>
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Sequential Processing
|
||||
Connect blocks in a chain where each block processes the output of the previous one:
|
||||
```
|
||||
User Input → Agent → Function → Response
|
||||
```
|
||||
|
||||
### Conditional Branching
|
||||
Use Condition or Router blocks to create different paths:
|
||||
```
|
||||
User Input → Router → Agent A (for questions)
|
||||
→ Agent B (for commands)
|
||||
```
|
||||
|
||||
### Quality Control
|
||||
Use Evaluator blocks to assess and filter outputs:
|
||||
```
|
||||
Agent → Evaluator → Condition → Response (if good)
|
||||
→ Agent (retry if bad)
|
||||
```
|
||||
|
||||
## Block Configuration
|
||||
|
||||
Each block type has its own configuration options allowing you to customize its behavior.
|
||||
Each block type has specific configuration options:
|
||||
|
||||
### Common Configuration Options
|
||||
**All Blocks**:
|
||||
- Input/output connections
|
||||
- Error handling behavior
|
||||
- Execution timeout settings
|
||||
|
||||
- **Input/output definitions**: Define how data flows in and out of the block
|
||||
- **Processing instructions**: Configure how the block processes its inputs
|
||||
- **API keys or authentication details**: Provide necessary credentials for external services
|
||||
- **Retry policies**: Configure how the block handles failures
|
||||
- **Error handling behavior**: Define how errors are managed and reported
|
||||
**AI Blocks** (Agent, Router, Evaluator):
|
||||
- Model selection (OpenAI, Anthropic, Google, local)
|
||||
- API keys and authentication
|
||||
- Temperature and other model parameters
|
||||
- System prompts and instructions
|
||||
|
||||
See the specific documentation for each block type to learn about its configuration options.
|
||||
**Logic Blocks** (Condition, Function):
|
||||
- Custom expressions or code
|
||||
- Variable references
|
||||
- Execution environment settings
|
||||
|
||||
**Integration Blocks** (API, Response):
|
||||
- Endpoint configuration
|
||||
- Headers and authentication
|
||||
- Request/response formatting
|
||||
|
||||
<Cards>
|
||||
<Card title="Agent Block" href="/blocks/agent">
|
||||
Connect to AI models and create intelligent responses
|
||||
</Card>
|
||||
<Card title="Function Block" href="/blocks/function">
|
||||
Run custom code to process and transform data
|
||||
</Card>
|
||||
<Card title="API Block" href="/blocks/api">
|
||||
Integrate with external services and APIs
|
||||
</Card>
|
||||
<Card title="Condition Block" href="/blocks/condition">
|
||||
Create branching logic based on data evaluation
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
175
apps/docs/content/docs/blocks/loop.mdx
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
title: Loop
|
||||
description: Create iterative workflows with loops that execute blocks repeatedly
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Loop block is a container block in Sim Studio that allows you to execute a group of blocks repeatedly. Loops enable iterative processing in your workflows.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/loop-light.png"
|
||||
darkSrc="/static/dark/loop-dark.png"
|
||||
alt="Loop Block"
|
||||
width={500}
|
||||
height={300}
|
||||
/>
|
||||
|
||||
<Callout type="info">
|
||||
Loop blocks are container nodes that can hold other blocks inside them. The blocks inside a loop will execute multiple times based on your configuration.
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
The Loop block enables you to:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Iterate over collections</strong>: Process arrays or objects one item at a time
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Repeat operations</strong>: Execute blocks a fixed number of times
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Loop Type
|
||||
|
||||
Choose between two types of loops:
|
||||
|
||||
<Tabs items={['For Loop', 'ForEach Loop']}>
|
||||
<Tab>
|
||||
A numeric loop that executes a fixed number of times. Use this when you need to repeat an operation a specific number of times.
|
||||
|
||||
```
|
||||
Example: Run 5 times
|
||||
- Iteration 1
|
||||
- Iteration 2
|
||||
- Iteration 3
|
||||
- Iteration 4
|
||||
- Iteration 5
|
||||
```
|
||||
</Tab>
|
||||
<Tab>
|
||||
A collection-based loop that iterates over each item in an array or object. Use this when you need to process a collection of items.
|
||||
|
||||
```
|
||||
Example: Process ["apple", "banana", "orange"]
|
||||
- Iteration 1: Process "apple"
|
||||
- Iteration 2: Process "banana"
|
||||
- Iteration 3: Process "orange"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## How to Use Loops
|
||||
|
||||
### Creating a Loop
|
||||
|
||||
1. Drag a Loop block from the toolbar onto your canvas
|
||||
2. Configure the loop type and parameters
|
||||
3. Drag other blocks inside the loop container
|
||||
4. Connect the blocks as needed
|
||||
|
||||
### Accessing Results
|
||||
|
||||
After a loop completes, you can access aggregated results:
|
||||
|
||||
- **`<loop.results>`**: Array of results from all loop iterations
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
### Processing API Results
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Process multiple customer records</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>API block fetches customer list</li>
|
||||
<li>ForEach loop iterates over each customer</li>
|
||||
<li>Inside loop: Agent analyzes customer data</li>
|
||||
<li>Inside loop: Function stores analysis results</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Iterative Content Generation
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Generate multiple variations</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Set For loop to 5 iterations</li>
|
||||
<li>Inside loop: Agent generates content variation</li>
|
||||
<li>Inside loop: Evaluator scores the content</li>
|
||||
<li>After loop: Function selects best variation</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Limitations
|
||||
|
||||
<Callout type="warning">
|
||||
Container blocks (Loops and Parallels) cannot be nested inside each other. This means:
|
||||
- You cannot place a Loop block inside another Loop block
|
||||
- You cannot place a Parallel block inside a Loop block
|
||||
- You cannot place any container block inside another container block
|
||||
|
||||
If you need multi-dimensional iteration, consider restructuring your workflow to use sequential loops or process data in stages.
|
||||
</Callout>
|
||||
|
||||
<Callout type="info">
|
||||
Loops execute sequentially, not in parallel. If you need concurrent execution, use the Parallel block instead.
|
||||
</Callout>
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
<Tabs items={['Configuration', 'Variables', 'Results']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Loop Type</strong>: Choose between 'for' or 'forEach'
|
||||
</li>
|
||||
<li>
|
||||
<strong>Iterations</strong>: Number of times to execute (for loops)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Collection</strong>: Array or object to iterate over (forEach loops)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>loop.currentItem</strong>: Current item being processed
|
||||
</li>
|
||||
<li>
|
||||
<strong>loop.index</strong>: Current iteration number (0-based)
|
||||
</li>
|
||||
<li>
|
||||
<strong>loop.items</strong>: Full collection (forEach loops)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>loop.results</strong>: Array of all iteration results
|
||||
</li>
|
||||
<li>
|
||||
<strong>Structure</strong>: Results maintain iteration order
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the loop
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Set reasonable limits**: Keep iteration counts reasonable to avoid long execution times
|
||||
- **Use ForEach for collections**: When processing arrays or objects, use ForEach instead of For loops
|
||||
- **Handle errors gracefully**: Consider adding error handling inside loops for robust workflows
|
||||
@@ -1,4 +1,15 @@
|
||||
{
|
||||
"title": "Blocks",
|
||||
"pages": ["agent", "api", "condition", "function", "evaluator", "router", "response", "workflow"]
|
||||
"pages": [
|
||||
"agent",
|
||||
"api",
|
||||
"condition",
|
||||
"function",
|
||||
"evaluator",
|
||||
"router",
|
||||
"response",
|
||||
"workflow",
|
||||
"loop",
|
||||
"parallel"
|
||||
]
|
||||
}
|
||||
|
||||
210
apps/docs/content/docs/blocks/parallel.mdx
Normal file
@@ -0,0 +1,210 @@
|
||||
---
|
||||
title: Parallel
|
||||
description: Execute multiple blocks concurrently for faster workflow processing
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Parallel block is a container block in Sim Studio that allows you to execute multiple instances of blocks concurrently.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/parallel-light.png"
|
||||
darkSrc="/static/dark/parallel-dark.png"
|
||||
alt="Parallel Block"
|
||||
width={500}
|
||||
height={300}
|
||||
/>
|
||||
|
||||
<Callout type="info">
|
||||
Parallel blocks are container nodes that execute their contents multiple times simultaneously, unlike loops which execute sequentially.
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
The Parallel block enables you to:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Distribute work</strong>: Process multiple items concurrently
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Speed up execution</strong>: Run independent operations simultaneously
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Handle bulk operations</strong>: Process large datasets efficiently
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Aggregate results</strong>: Collect outputs from all parallel executions
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Parallel Type
|
||||
|
||||
Choose between two types of parallel execution:
|
||||
|
||||
<Tabs items={['Count-based', 'Collection-based']}>
|
||||
<Tab>
|
||||
Execute a fixed number of parallel instances. Use this when you need to run the same operation multiple times concurrently.
|
||||
|
||||
```
|
||||
Example: Run 5 parallel instances
|
||||
- Instance 1 ┐
|
||||
- Instance 2 ├─ All execute simultaneously
|
||||
- Instance 3 │
|
||||
- Instance 4 │
|
||||
- Instance 5 ┘
|
||||
```
|
||||
</Tab>
|
||||
<Tab>
|
||||
Distribute a collection across parallel instances. Each instance processes one item from the collection simultaneously.
|
||||
|
||||
```
|
||||
Example: Process ["task1", "task2", "task3"] in parallel
|
||||
- Instance 1: Process "task1" ┐
|
||||
- Instance 2: Process "task2" ├─ All execute simultaneously
|
||||
- Instance 3: Process "task3" ┘
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## How to Use Parallel Blocks
|
||||
|
||||
### Creating a Parallel Block
|
||||
|
||||
1. Drag a Parallel block from the toolbar onto your canvas
|
||||
2. Configure the parallel type and parameters
|
||||
3. Drag a single block inside the parallel container
|
||||
4. Connect the block as needed
|
||||
|
||||
### Accessing Results
|
||||
|
||||
After a parallel block completes, you can access aggregated results:
|
||||
|
||||
- **`<parallel.results>`**: Array of results from all parallel instances
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
### Batch API Processing
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Process multiple API calls simultaneously</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Parallel block with collection of API endpoints</li>
|
||||
<li>Inside parallel: API block calls each endpoint</li>
|
||||
<li>After parallel: Process all responses together</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Multi-Model AI Processing
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Get responses from multiple AI models</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Count-based parallel set to 3 instances</li>
|
||||
<li>Inside parallel: Agent configured with different model per instance</li>
|
||||
<li>After parallel: Compare and select best response</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Result Aggregation
|
||||
|
||||
Results from all parallel instances are automatically collected:
|
||||
|
||||
```javascript
|
||||
// In a Function block after the parallel
|
||||
const allResults = input.parallel.results;
|
||||
// Returns: [result1, result2, result3, ...]
|
||||
```
|
||||
|
||||
### Instance Isolation
|
||||
|
||||
Each parallel instance runs independently:
|
||||
- Separate variable scopes
|
||||
- No shared state between instances
|
||||
- Failures in one instance don't affect others
|
||||
|
||||
### Limitations
|
||||
|
||||
<Callout type="warning">
|
||||
Container blocks (Loops and Parallels) cannot be nested inside each other. This means:
|
||||
- You cannot place a Loop block inside a Parallel block
|
||||
- You cannot place another Parallel block inside a Parallel block
|
||||
- You cannot place any container block inside another container block
|
||||
</Callout>
|
||||
|
||||
<Callout type="warning">
|
||||
Parallel blocks can only contain a single block. You cannot have multiple blocks connected to each other inside a parallel - only the first block would execute in that case.
|
||||
</Callout>
|
||||
|
||||
<Callout type="info">
|
||||
While parallel execution is faster, be mindful of:
|
||||
- API rate limits when making concurrent requests
|
||||
- Memory usage with large datasets
|
||||
- Maximum of 20 concurrent instances to prevent resource exhaustion
|
||||
</Callout>
|
||||
|
||||
## Parallel vs Loop
|
||||
|
||||
Understanding when to use each:
|
||||
|
||||
| Feature | Parallel | Loop |
|
||||
|---------|----------|------|
|
||||
| Execution | Concurrent | Sequential |
|
||||
| Speed | Faster for independent operations | Slower but ordered |
|
||||
| Order | No guaranteed order | Maintains order |
|
||||
| Use case | Independent operations | Dependent operations |
|
||||
| Resource usage | Higher | Lower |
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
<Tabs items={['Configuration', 'Variables', 'Results']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Parallel Type</strong>: Choose between 'count' or 'collection'
|
||||
</li>
|
||||
<li>
|
||||
<strong>Count</strong>: Number of instances to run (count-based)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Collection</strong>: Array or object to distribute (collection-based)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>parallel.currentItem</strong>: Item for this instance
|
||||
</li>
|
||||
<li>
|
||||
<strong>parallel.index</strong>: Instance number (0-based)
|
||||
</li>
|
||||
<li>
|
||||
<strong>parallel.items</strong>: Full collection (collection-based)
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>parallel.results</strong>: Array of all instance results
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the parallel
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Independent operations only**: Ensure operations don't depend on each other
|
||||
- **Handle rate limits**: Add delays or throttling for API-heavy workflows
|
||||
- **Error handling**: Each instance should handle its own errors gracefully
|
||||
@@ -8,38 +8,40 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Response block is the final component in API-enabled workflows that transforms your workflow's variables into a structured HTTP response. This block serves as the endpoint that returns data, status codes, and headers back to API callers.
|
||||
The Response block is the final step in your workflow that formats and returns data to whoever called your workflow. It's like the "return" statement for your entire workflow—it packages up results and sends them back.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/response-light.png"
|
||||
darkSrc="/static/dark/response-dark.png"
|
||||
alt="Response Block"
|
||||
width={430}
|
||||
height={784}
|
||||
alt="Response Block Configuration"
|
||||
width={350}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
<Callout type="info">
|
||||
Response blocks are terminal blocks - they mark the end of a workflow execution and cannot have further connections.
|
||||
Response blocks are terminal blocks - they end the workflow execution and cannot connect to other blocks.
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
## When You Need Response Blocks
|
||||
|
||||
The Response block serves as the final output mechanism for API workflows, enabling you to:
|
||||
**API Endpoints**: When your workflow is called via API, Response blocks format the return data
|
||||
**Webhooks**: Return confirmation or data back to the calling system
|
||||
**Testing**: See formatted results when testing your workflow
|
||||
**Data Export**: Structure data for external systems or reports
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Return structured data</strong>: Transform workflow variables into JSON responses
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Set HTTP status codes</strong>: Control the response status (200, 400, 500, etc.)
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Configure headers</strong>: Add custom HTTP headers to the response
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Reference variables</strong>: Use workflow variables dynamically in the response
|
||||
</Step>
|
||||
</Steps>
|
||||
## Two Ways to Build Responses
|
||||
|
||||
### Builder Mode (Recommended)
|
||||
Visual interface for building response structure:
|
||||
- Drag and drop fields
|
||||
- Reference workflow variables easily
|
||||
- Visual preview of response structure
|
||||
|
||||
### Editor Mode (Advanced)
|
||||
Write JSON directly:
|
||||
- Full control over response format
|
||||
- Support for complex nested structures
|
||||
- Use `<variable.name>` syntax for dynamic values
|
||||
|
||||
## Configuration Options
|
||||
|
||||
|
||||
@@ -6,35 +6,73 @@ description: Route workflow execution based on specific conditions or logic
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
The Router block is a powerful component in Sim Studio that intelligently routes workflow execution based on content analysis, user input, or predefined conditions. It acts as a decision-making junction in your workflow, directing the flow to different paths based on various criteria.
|
||||
The Router block uses AI to intelligently decide which path your workflow should take next. Unlike Condition blocks that use simple rules, Router blocks can understand context and make smart routing decisions based on content analysis.
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/router-light.png"
|
||||
darkSrc="/static/dark/router-dark.png"
|
||||
alt="Router Block"
|
||||
width={300}
|
||||
alt="Router Block with Multiple Paths"
|
||||
width={350}
|
||||
height={175}
|
||||
/>
|
||||
|
||||
## Overview
|
||||
|
||||
The Router block uses LLMs to analyze input content and determine the most appropriate next step in your workflow. This allows for:
|
||||
The Router block enables you to:
|
||||
|
||||
- Creating dynamic, adaptable workflows
|
||||
- Implementing complex decision trees
|
||||
- Routing user requests to specialized components
|
||||
- Building conversational systems that can handle diverse inputs
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Intelligent content routing</strong>: Use AI to understand intent and context
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Dynamic path selection</strong>: Route workflows based on unstructured content analysis
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Context-aware decisions</strong>: Make smart routing choices beyond simple rules
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Multi-path management</strong>: Handle complex workflows with multiple potential destinations
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Router vs Condition Blocks
|
||||
|
||||
<Accordions>
|
||||
<Accordion title="When to Use Router">
|
||||
- AI-powered content analysis needed
|
||||
- Unstructured or varying content types
|
||||
- Intent-based routing (e.g., "route support tickets to departments")
|
||||
- Context-aware decision making required
|
||||
</Accordion>
|
||||
<Accordion title="When to Use Condition">
|
||||
- Simple, rule-based decisions
|
||||
- Structured data or numeric comparisons
|
||||
- Fast, deterministic routing needed
|
||||
- Boolean logic sufficient
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
## How It Works
|
||||
|
||||
The Router block:
|
||||
|
||||
1. Analyzes the input content using an LLM
|
||||
2. Evaluates the content against the available target blocks in your workflow
|
||||
3. Identifies the most appropriate destination based on the content's intent or requirements
|
||||
4. Routes the workflow execution to the selected block
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Analyze content</strong>: Uses an LLM to understand input content and context
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Evaluate targets</strong>: Compares content against available destination blocks
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Select destination</strong>: Identifies the most appropriate path based on intent
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Route execution</strong>: Directs workflow to the selected block
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Configuration Options
|
||||
|
||||
@@ -56,65 +94,165 @@ The possible destination blocks that the Router can select from. The Router will
|
||||
|
||||
### Model Selection
|
||||
|
||||
Choose an LLM provider to power the routing decision:
|
||||
Choose an AI model to power the routing decision:
|
||||
|
||||
- OpenAI (GPT-4o, o1, o3, o4-mini)
|
||||
- Anthropic (Claude 3.7 Sonnet)
|
||||
- Google (Gemini 2.5 Pro, Gemini 2.0 Flash)
|
||||
- Groq, Cerebras
|
||||
- Ollama Local Models
|
||||
- And more
|
||||
**OpenAI**: GPT-4o, o1, o3, o4-mini, gpt-4.1 \
|
||||
**Anthropic**: Claude 3.7 Sonnet \
|
||||
**Google**: Gemini 2.5 Pro, Gemini 2.0 Flash \
|
||||
**Other Providers**: Groq, Cerebras, xAI, DeepSeek \
|
||||
**Local Models**: Any model running on Ollama
|
||||
|
||||
Select a model with strong reasoning capabilities for more accurate routing decisions.
|
||||
<div className="w-full max-w-2xl mx-auto overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/router-model-dropdown.mp4"></video>
|
||||
</div>
|
||||
|
||||
**Recommendation**: Use models with strong reasoning capabilities like GPT-4o or Claude 3.7 Sonnet for more accurate routing decisions.
|
||||
|
||||
### API Key
|
||||
|
||||
Your API key for the selected LLM provider. This is securely stored and used for authentication.
|
||||
|
||||
## Inputs and Outputs
|
||||
### Accessing Results
|
||||
|
||||
### Inputs
|
||||
After a router makes a decision, you can access its outputs:
|
||||
|
||||
- **Content/Prompt**: The text to analyze for routing decisions
|
||||
- **Target Blocks**: Connected blocks that are potential routing destinations
|
||||
- **Model Settings**: LLM provider and parameters
|
||||
- **`<router.content>`**: Summary of the routing decision made
|
||||
- **`<router.selected_path>`**: Details of the chosen destination block
|
||||
- **`<router.tokens>`**: Token usage statistics from the LLM
|
||||
- **`<router.model>`**: The model used for decision-making
|
||||
|
||||
### Outputs
|
||||
## Advanced Features
|
||||
|
||||
- **Content**: A summary of the routing decision
|
||||
- **Model**: The model used for decision-making
|
||||
- **Tokens**: Usage statistics
|
||||
- **Selected Path**: Details of the chosen routing destination, including:
|
||||
- Block ID
|
||||
- Block Type
|
||||
- Block Title
|
||||
### Custom Routing Criteria
|
||||
|
||||
## Example Usage
|
||||
Define specific criteria for each target block:
|
||||
|
||||
Here's an example of how a Router block might be used in a customer support workflow:
|
||||
|
||||
```yaml
|
||||
# Example Router Configuration
|
||||
prompt: |
|
||||
Analyze the user query and route to the most appropriate department.
|
||||
Choose ONE destination based on the query content and intent.
|
||||
|
||||
model: OpenAI/gpt-4
|
||||
```javascript
|
||||
// Example routing descriptions
|
||||
Target Block 1: "Technical support issues, API problems, integration questions"
|
||||
Target Block 2: "Billing inquiries, subscription changes, payment issues"
|
||||
Target Block 3: "General questions, feedback, feature requests"
|
||||
```
|
||||
|
||||
In this example, the Router might be connected to:
|
||||
### Multi-Model Routing
|
||||
|
||||
- A product support block
|
||||
- A billing inquiries block
|
||||
- A technical support block
|
||||
- A general inquiries block
|
||||
Use different models for different routing scenarios:
|
||||
|
||||
```javascript
|
||||
// Fast routing for simple cases
|
||||
Model: GPT-4o-mini
|
||||
Criteria: Simple, common routing patterns
|
||||
|
||||
// Complex routing for nuanced decisions
|
||||
Model: Claude 3.7 Sonnet
|
||||
Criteria: Complex content analysis required
|
||||
```
|
||||
|
||||
### Fallback Handling
|
||||
|
||||
Implement robust fallback mechanisms:
|
||||
|
||||
```javascript
|
||||
// Router configuration
|
||||
Primary Targets: ["Support", "Sales", "Technical"]
|
||||
Fallback Target: "General" // Default when no specific match
|
||||
Confidence Threshold: 0.7 // Minimum confidence for routing
|
||||
```
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
<Tabs items={['Configuration', 'Variables', 'Results']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Content/Prompt</strong>: Text to analyze for routing decisions
|
||||
</li>
|
||||
<li>
|
||||
<strong>Target Blocks</strong>: Connected blocks as potential destinations
|
||||
</li>
|
||||
<li>
|
||||
<strong>Model</strong>: AI model for routing analysis
|
||||
</li>
|
||||
<li>
|
||||
<strong>API Key</strong>: Authentication for selected LLM provider
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>router.content</strong>: Summary of routing decision
|
||||
</li>
|
||||
<li>
|
||||
<strong>router.selected_path</strong>: Details of chosen destination
|
||||
</li>
|
||||
<li>
|
||||
<strong>router.tokens</strong>: Token usage statistics
|
||||
</li>
|
||||
<li>
|
||||
<strong>router.model</strong>: Model used for decision-making
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Routing Decision</strong>: Primary path selection result
|
||||
</li>
|
||||
<li>
|
||||
<strong>Decision Context</strong>: Analysis summary and reasoning
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the router
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
### Customer Support Triage
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Route support tickets to specialized departments</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>User submits support request via form</li>
|
||||
<li>Router analyzes ticket content and context</li>
|
||||
<li>Technical issues → Engineering support agent</li>
|
||||
<li>Billing questions → Finance support agent</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Content Classification
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Classify and route user-generated content</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>User submits content or feedback</li>
|
||||
<li>Router analyzes content type and sentiment</li>
|
||||
<li>Feature requests → Product team workflow</li>
|
||||
<li>Bug reports → Technical support workflow</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Lead Qualification
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Route leads based on qualification criteria</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Lead information captured from form</li>
|
||||
<li>Router analyzes company size, industry, and needs</li>
|
||||
<li>Enterprise leads → Sales team with custom pricing</li>
|
||||
<li>SMB leads → Self-service onboarding flow</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
Based on the user's query, the Router would analyze the content and direct it to the most appropriate specialized support block.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Provide clear descriptions for target blocks**: Help the Router understand when to select each destination
|
||||
- **Use specific routing criteria**: Define clear conditions for selecting each path
|
||||
- **Consider fallback paths**: Connect a default destination for when no specific path is appropriate
|
||||
- **Test with diverse inputs**: Ensure the Router handles various input types correctly
|
||||
- **Review routing decisions**: Monitor the Router's performance and refine as needed
|
||||
- **Provide clear target descriptions**: Help the Router understand when to select each destination with specific, detailed descriptions
|
||||
- **Use specific routing criteria**: Define clear conditions and examples for each path to improve accuracy
|
||||
- **Implement fallback paths**: Connect a default destination for when no specific path is appropriate
|
||||
- **Test with diverse inputs**: Ensure the Router handles various input types, edge cases, and unexpected content
|
||||
- **Monitor routing performance**: Review routing decisions regularly and refine criteria based on actual usage patterns
|
||||
- **Choose appropriate models**: Use models with strong reasoning capabilities for complex routing decisions
|
||||
|
||||
@@ -71,12 +71,15 @@ Define the data to pass to the child workflow:
|
||||
- **Optional**: The input field is optional - child workflows can run without input data
|
||||
- **Type Preservation**: Variable types (strings, numbers, objects, etc.) are preserved when passed to the child workflow
|
||||
|
||||
### Examples of Input References
|
||||
### Accessing Results
|
||||
|
||||
- `<variable.customerData>` - Pass a workflow variable
|
||||
- `<dataProcessor.result>` - Pass the result from a previous block
|
||||
- `<start.input>` - Pass the original workflow input
|
||||
- `<apiCall.data.user>` - Pass a specific field from an API response
|
||||
After a workflow executes, you can access its outputs:
|
||||
|
||||
- **`<workflow.response>`**: The complete output from the child workflow
|
||||
- **`<workflow.name>`**: The name of the executed child workflow
|
||||
- **`<workflow.success>`**: Boolean indicating successful completion
|
||||
- **`<workflow.error>`**: Error details if the workflow failed
|
||||
- **`<workflow.execution_time>`**: Time taken to execute the workflow
|
||||
|
||||
### Execution Context
|
||||
|
||||
@@ -100,132 +103,157 @@ To prevent infinite recursion and ensure system stability, the Workflow block in
|
||||
- **Timeout Protection**: Child workflows inherit timeout settings to prevent indefinite execution
|
||||
- **Resource Limits**: Memory and execution time limits apply to prevent resource exhaustion
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Dynamic Workflow Selection
|
||||
|
||||
Select workflows dynamically based on runtime conditions:
|
||||
|
||||
```javascript
|
||||
// In a Function block before the Workflow block
|
||||
const workflowId = <condition.result> ? 'premium-workflow' : 'standard-workflow';
|
||||
return { selectedWorkflow: workflowId };
|
||||
```
|
||||
|
||||
### Error Handling and Fallbacks
|
||||
|
||||
Implement robust error handling for child workflows:
|
||||
|
||||
```javascript
|
||||
// In a Function block after the Workflow block
|
||||
if (!<workflow.success>) {
|
||||
console.error('Child workflow failed:', <workflow.error>);
|
||||
// Implement fallback logic
|
||||
return { fallback: true, error: <workflow.error> };
|
||||
}
|
||||
return <workflow.response>;
|
||||
```
|
||||
|
||||
### Workflow Chaining
|
||||
|
||||
Chain multiple workflows together:
|
||||
|
||||
```javascript
|
||||
// Pass output from one workflow to another
|
||||
Workflow 1 Input: <start.input>
|
||||
Workflow 2 Input: <workflow1.response>
|
||||
Workflow 3 Input: <workflow2.response>
|
||||
```
|
||||
|
||||
## Inputs and Outputs
|
||||
|
||||
<Tabs items={['Inputs', 'Outputs']}>
|
||||
<Tabs items={['Configuration', 'Variables', 'Results']}>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Workflow ID</strong>: The identifier of the workflow to execute
|
||||
<strong>Workflow Selection</strong>: Choose which workflow to execute
|
||||
</li>
|
||||
<li>
|
||||
<strong>Input Variable</strong>: Variable or block reference to pass to the child workflow (e.g., `<variable.name>` or `<block.field>`)
|
||||
<strong>Input Data</strong>: Variable or block reference to pass to child workflow
|
||||
</li>
|
||||
<li>
|
||||
<strong>Execution Context</strong>: Isolated environment with workspace resources
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Response</strong>: The complete output from the child workflow execution
|
||||
<strong>workflow.response</strong>: Complete output from child workflow
|
||||
</li>
|
||||
<li>
|
||||
<strong>Child Workflow Name</strong>: The name of the executed child workflow
|
||||
<strong>workflow.name</strong>: Name of executed child workflow
|
||||
</li>
|
||||
<li>
|
||||
<strong>Success Status</strong>: Boolean indicating whether the child workflow completed successfully
|
||||
<strong>workflow.success</strong>: Boolean indicating completion status
|
||||
</li>
|
||||
<li>
|
||||
<strong>Error Information</strong>: Details about any errors that occurred during execution
|
||||
<strong>workflow.error</strong>: Error details if workflow failed
|
||||
</li>
|
||||
<li>
|
||||
<strong>Execution Metadata</strong>: Information about execution time, resource usage, and performance
|
||||
<strong>workflow.execution_time</strong>: Time taken to execute
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
<li>
|
||||
<strong>Workflow Response</strong>: Primary output from child workflow
|
||||
</li>
|
||||
<li>
|
||||
<strong>Execution Status</strong>: Success status and error information
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access</strong>: Available in blocks after the workflow
|
||||
</li>
|
||||
</ul>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Example Usage
|
||||
## Example Use Cases
|
||||
|
||||
Here's an example of how a Workflow block might be used to create a modular customer onboarding process:
|
||||
### Modular Customer Onboarding
|
||||
|
||||
### Parent Workflow: Customer Onboarding
|
||||
```yaml
|
||||
# Main customer onboarding workflow
|
||||
blocks:
|
||||
- type: workflow
|
||||
name: "Validate Customer Data"
|
||||
workflowId: "customer-validation-workflow"
|
||||
input: "<variable.newCustomer>"
|
||||
|
||||
- type: workflow
|
||||
name: "Setup Customer Account"
|
||||
workflowId: "account-setup-workflow"
|
||||
input: "<Validate Customer Data.result>"
|
||||
|
||||
- type: workflow
|
||||
name: "Send Welcome Email"
|
||||
workflowId: "welcome-email-workflow"
|
||||
input: "<Setup Customer Account.result.accountDetails>"
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Break down complex onboarding into reusable components</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Main workflow receives customer data</li>
|
||||
<li>Workflow block executes validation workflow</li>
|
||||
<li>Workflow block executes account setup workflow</li>
|
||||
<li>Workflow block executes welcome email workflow</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Microservice Architecture
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Create independent service workflows</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Payment processing workflow handles transactions</li>
|
||||
<li>Inventory management workflow updates stock</li>
|
||||
<li>Notification workflow sends confirmations</li>
|
||||
<li>Main workflow orchestrates all services</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Conditional Processing
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Scenario: Execute different workflows based on conditions</h4>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Condition block evaluates user type</li>
|
||||
<li>Enterprise users → Complex approval workflow</li>
|
||||
<li>Standard users → Simple approval workflow</li>
|
||||
<li>Free users → Basic processing workflow</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Example: Customer Validation Workflow
|
||||
|
||||
```javascript title="validation-workflow.js"
|
||||
// Main workflow passes customer data to validation workflow
|
||||
const customerData = <start.input>;
|
||||
|
||||
// Validation workflow processes the data
|
||||
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(customerData.email);
|
||||
const phoneValid = /^\+?[1-9]\d{1,14}$/.test(customerData.phone);
|
||||
|
||||
return {
|
||||
customer: customerData,
|
||||
validation: {
|
||||
email: emailValid,
|
||||
phone: phoneValid,
|
||||
overall: emailValid && phoneValid
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Child Workflow: Customer Validation
|
||||
```yaml
|
||||
# Reusable customer validation workflow
|
||||
# Access the input data using: start.input
|
||||
blocks:
|
||||
- type: function
|
||||
name: "Validate Email"
|
||||
code: |
|
||||
const customerData = start.input;
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(customerData.email);
|
||||
|
||||
- type: api
|
||||
name: "Check Credit Score"
|
||||
url: "https://api.creditcheck.com/score"
|
||||
method: "POST"
|
||||
body: "<start.input>"
|
||||
```
|
||||
|
||||
### Variable Reference Examples
|
||||
|
||||
```yaml
|
||||
# Using workflow variables
|
||||
input: "<variable.customerInfo>"
|
||||
|
||||
# Using block outputs
|
||||
input: "<dataProcessor.cleanedData>"
|
||||
|
||||
# Using nested object properties
|
||||
input: "<apiCall.data.user.profile>"
|
||||
|
||||
# Using array elements (if supported by the resolver)
|
||||
input: "<listProcessor.items[0]>"
|
||||
```
|
||||
|
||||
## Access Control and Permissions
|
||||
|
||||
The Workflow block respects workspace permissions and access controls:
|
||||
|
||||
- **Workspace Membership**: Only workflows within the same workspace can be executed
|
||||
- **Permission Inheritance**: Child workflows inherit the execution permissions of the parent workflow
|
||||
- **API Key Access**: Child workflows have access to the same API keys and environment variables as the parent
|
||||
- **User Context**: The execution maintains the original user context for audit and logging purposes
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Keep workflows focused**: Design child workflows to handle specific, well-defined tasks
|
||||
- **Minimize nesting depth**: Avoid deeply nested workflow hierarchies for better maintainability
|
||||
- **Handle errors gracefully**: Implement proper error handling for child workflow failures
|
||||
- **Document dependencies**: Clearly document which workflows depend on others
|
||||
- **Version control**: Consider versioning strategies for workflows that are used as components
|
||||
- **Test independently**: Ensure child workflows can be tested and validated independently
|
||||
- **Monitor performance**: Be aware that nested workflows can impact overall execution time
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Microservice Architecture
|
||||
Break down complex business processes into smaller, focused workflows that can be developed and maintained independently.
|
||||
|
||||
### Reusable Components
|
||||
Create library workflows for common operations like data validation, email sending, or API integrations that can be reused across multiple projects.
|
||||
|
||||
### Conditional Execution
|
||||
Use workflow blocks within conditional logic to execute different business processes based on runtime conditions.
|
||||
|
||||
### Parallel Processing
|
||||
Combine workflow blocks with parallel execution to run multiple child workflows simultaneously for improved performance.
|
||||
|
||||
<Callout type="tip">
|
||||
When designing modular workflows, think of each workflow as a function with clear inputs, outputs, and a single responsibility.
|
||||
</Callout>
|
||||
- **Keep workflows focused**: Design child workflows to handle specific, well-defined tasks with clear inputs and outputs
|
||||
- **Minimize nesting depth**: Avoid deeply nested workflow hierarchies for better maintainability and performance
|
||||
- **Handle errors gracefully**: Implement proper error handling for child workflow failures and provide fallback mechanisms
|
||||
- **Document dependencies**: Clearly document which workflows depend on others and maintain dependency maps
|
||||
- **Test independently**: Ensure child workflows can be tested and validated independently from parent workflows
|
||||
- **Monitor performance**: Be aware that nested workflows can impact overall execution time and resource usage
|
||||
- **Use semantic naming**: Give workflows descriptive names that clearly indicate their purpose and functionality
|
||||
@@ -71,7 +71,6 @@ Different block types produce different output structures. Here's what you can e
|
||||
{
|
||||
"result": "Function return value",
|
||||
"stdout": "Console output",
|
||||
"executionTime": 45
|
||||
}
|
||||
```
|
||||
|
||||
@@ -79,7 +78,6 @@ Different block types produce different output structures. Here's what you can e
|
||||
|
||||
- **result**: The return value of the function (can be any type)
|
||||
- **stdout**: Console output captured during function execution
|
||||
- **executionTime**: Time taken to execute the function (in milliseconds)
|
||||
|
||||
</Tab>
|
||||
<Tab>
|
||||
|
||||
@@ -14,8 +14,8 @@ Connections are the pathways that allow data to flow between blocks in your work
|
||||
data moves through your system and how blocks interact with each other.
|
||||
</Callout>
|
||||
|
||||
<div>
|
||||
<video autoPlay loop muted playsInline className="w-full" src="/connections.mp4"></video>
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/connections.mp4"></video>
|
||||
</div>
|
||||
|
||||
## Connection Types
|
||||
|
||||
@@ -7,8 +7,8 @@ import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Connection tags are visual representations of the data available from connected blocks. They provide an easy way to reference outputs from previous blocks in your workflow.
|
||||
|
||||
<div>
|
||||
<video autoPlay loop muted playsInline className="w-full" src="/connections.mp4"></video>
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/connections.mp4"></video>
|
||||
</div>
|
||||
|
||||
### What Are Connection Tags?
|
||||
|
||||
@@ -1,280 +1,208 @@
|
||||
---
|
||||
title: Advanced Execution Features
|
||||
description: Master advanced execution capabilities in Sim Studio
|
||||
title: Logging and Cost Calculation
|
||||
description: Understanding workflow logs and how execution costs are calculated in Sim Studio
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
|
||||
Sim Studio provides several advanced features that give you more control over workflow execution, error handling, and performance optimization.
|
||||
Sim Studio provides comprehensive logging for workflow executions and automatic cost calculation for AI model usage.
|
||||
|
||||
## Error Handling
|
||||
## Logging System
|
||||
|
||||
The execution engine includes built-in error handling mechanisms to make your workflows more robust:
|
||||
Sim Studio offers two complementary logging interfaces:
|
||||
|
||||
### Block-Level Error Handling
|
||||
### Real-Time Console (Manual Executions)
|
||||
|
||||
Errors in one block don't necessarily stop the entire workflow execution:
|
||||
During manual workflow execution, logs appear in real-time in the Console panel on the right side of the workflow editor:
|
||||
|
||||
```javascript
|
||||
// Example of error handling in a Function block
|
||||
try {
|
||||
// Potentially risky operation
|
||||
const result = JSON.parse(input.apiBlock.data);
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
// Handle the error gracefully
|
||||
console.error("Failed to parse JSON:", error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
fallbackData: { status: "error", message: "Could not process data" }
|
||||
};
|
||||
}
|
||||
```
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/console-panel-light.png"
|
||||
darkSrc="/static/dark/console-panel-dark.png"
|
||||
alt="Real-time Console Panel"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
### Error Logging
|
||||
The console shows:
|
||||
- Block execution progress with active block highlighting
|
||||
- Real-time outputs as blocks complete
|
||||
- Execution timing for each block
|
||||
- Success/error status indicators
|
||||
|
||||
Comprehensive error information is captured in the execution logs:
|
||||
### Logs Page (All Executions)
|
||||
|
||||
- **Error Messages**: Clear descriptions of what went wrong
|
||||
- **Stack Traces**: Detailed information about where errors occurred
|
||||
- **Context Data**: The inputs that led to the error
|
||||
- **Timestamps**: When the error occurred
|
||||
All workflow executions—whether triggered manually, via API, Chat, Schedule, or Webhook—are logged to the dedicated Logs page:
|
||||
|
||||
<Callout type="info">
|
||||
Error logs are invaluable for debugging workflows. Always check the logs first when
|
||||
troubleshooting execution issues.
|
||||
</Callout>
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/logs-page-light.png"
|
||||
darkSrc="/static/dark/logs-page-dark.png"
|
||||
alt="Logs Page"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
### Fallback Mechanisms
|
||||
The Logs page provides:
|
||||
- Comprehensive filtering by time range, status, trigger type, folder, and workflow
|
||||
- Search functionality across all logs
|
||||
- Live mode for real-time updates
|
||||
- 7-day log retention (upgradeable for longer retention)
|
||||
|
||||
For certain operations, the system provides automatic fallbacks:
|
||||
## Log Details Sidebar
|
||||
|
||||
- **Function Execution**: Freestyle execution first, then VM execution if needed
|
||||
- **API Requests**: Automatic retries for transient network errors
|
||||
- **Model Calls**: Fallback to alternative models if primary model is unavailable
|
||||
Clicking on any log entry opens a detailed sidebar view:
|
||||
|
||||
### Recovery Options
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/logs-sidebar-light.png"
|
||||
darkSrc="/static/dark/logs-sidebar-dark.png"
|
||||
alt="Logs Sidebar Details"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
Configure blocks to handle failures gracefully:
|
||||
### Block Input/Output
|
||||
|
||||
- **Retry Logic**: Automatically retry failed operations
|
||||
- **Default Values**: Provide fallback values when operations fail
|
||||
- **Alternative Paths**: Use conditional blocks to create error handling paths
|
||||
- **Graceful Degradation**: Continue execution with partial results
|
||||
View the complete data flow for each block with tabs to switch between:
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Environment variables provide a secure way to store and access configuration values:
|
||||
|
||||
### Types of Environment Variables
|
||||
|
||||
<Tabs items={['API Keys', 'Configuration Values', 'Secrets']}>
|
||||
<Tabs items={['Output', 'Input']}>
|
||||
<Tab>
|
||||
Store API credentials securely: ``` OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-...
|
||||
GOOGLE_API_KEY=AIza... ``` These are automatically available to blocks that need them, without
|
||||
hardcoding sensitive values in your workflow.
|
||||
**Output Tab** shows the block's execution result:
|
||||
- Structured data with JSON formatting
|
||||
- Markdown rendering for AI-generated content
|
||||
- Copy button for easy data extraction
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
Manage environment-specific configuration: ``` MAX_RETRIES=3 DEFAULT_MODEL=gpt-4o LOG_LEVEL=info
|
||||
BASE_URL=https://api.example.com ``` These values can be referenced in blocks to control behavior
|
||||
without modifying the workflow itself.
|
||||
</Tab>
|
||||
|
||||
|
||||
<Tab>
|
||||
Store sensitive information securely: ``` DATABASE_PASSWORD=... JWT_SECRET=...
|
||||
ENCRYPTION_KEY=... ``` These values are encrypted at rest and only decrypted during execution,
|
||||
providing an extra layer of security.
|
||||
**Input Tab** displays what was passed to the block:
|
||||
- Resolved variable values
|
||||
- Referenced outputs from other blocks
|
||||
- Environment variables used
|
||||
- API keys are automatically redacted for security
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Using Environment Variables
|
||||
### Execution Timeline
|
||||
|
||||
Environment variables can be accessed in different ways depending on the block type:
|
||||
For workflow-level logs, view detailed execution metrics:
|
||||
- Start and end timestamps
|
||||
- Total workflow duration
|
||||
- Individual block execution times
|
||||
- Performance bottleneck identification
|
||||
|
||||
```javascript
|
||||
// In Function blocks
|
||||
const apiKey = process.env.MY_API_KEY
|
||||
const maxRetries = parseInt(process.env.MAX_RETRIES || '3')
|
||||
### Model Breakdown
|
||||
|
||||
// In API blocks (via connection tags)
|
||||
// URL: https://api.example.com?key=<env.MY_API_KEY>
|
||||
For workflows using AI blocks, expand the Model Breakdown section to see:
|
||||
|
||||
// In Agent blocks (via connection tags)
|
||||
// System prompt: Use the model <env.DEFAULT_MODEL> for this task.
|
||||
```
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/model-breakdown-light.png"
|
||||
darkSrc="/static/dark/model-breakdown-dark.png"
|
||||
alt="Model Breakdown"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
<Callout type="warning">
|
||||
Never hardcode sensitive information like API keys directly in your workflows. Always use
|
||||
environment variables instead.
|
||||
- **Token Usage**: Input and output token counts for each model
|
||||
- **Cost Breakdown**: Individual costs per model and operation
|
||||
- **Model Distribution**: Which models were used and how many times
|
||||
- **Total Cost**: Aggregate cost for the entire workflow execution
|
||||
|
||||
### Workflow Snapshot
|
||||
|
||||
For any logged execution, click "View Snapshot" to see the exact workflow state at execution time:
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/workflow-snapshot-light.png"
|
||||
darkSrc="/static/dark/workflow-snapshot-dark.png"
|
||||
alt="Workflow Snapshot"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
The snapshot provides:
|
||||
- Frozen canvas showing the workflow structure
|
||||
- Block states and connections as they were during execution
|
||||
- Click any block to see its inputs and outputs
|
||||
- Useful for debugging workflows that have since been modified
|
||||
|
||||
<Callout type="info">
|
||||
Workflow snapshots are only available for executions after the enhanced logging system was introduced. Older migrated logs show a "Logged State Not Found" message.
|
||||
</Callout>
|
||||
|
||||
## Real-Time Monitoring
|
||||
## Cost Calculation
|
||||
|
||||
Sim Studio provides powerful real-time monitoring capabilities:
|
||||
Sim Studio automatically calculates costs for all AI model usage:
|
||||
|
||||
### How Costs Are Calculated
|
||||
|
||||
```javascript
|
||||
cost = (inputTokens × inputPrice + outputTokens × outputPrice) / 1,000,000
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Prices are per million tokens. The calculation divides by 1,000,000 to get the actual cost.
|
||||
</Callout>
|
||||
|
||||
### Pricing Options
|
||||
|
||||
<Tabs items={['Hosted Models', 'Bring Your Own API Key']}>
|
||||
<Tab>
|
||||
**Hosted Models** - Sim Studio provides API keys with a 2.5x pricing multiplier:
|
||||
|
||||
| Model | Base Price (Input/Output) | Hosted Price (Input/Output) |
|
||||
|-------|---------------------------|----------------------------|
|
||||
| GPT-4o | $2.50 / $10.00 | $6.25 / $25.00 |
|
||||
| GPT-4.1 | $2.00 / $8.00 | $5.00 / $20.00 |
|
||||
| o1 | $15.00 / $60.00 | $37.50 / $150.00 |
|
||||
| o3 | $2.00 / $8.00 | $5.00 / $20.00 |
|
||||
| Claude 3.5 Sonnet | $3.00 / $15.00 | $7.50 / $37.50 |
|
||||
| Claude Opus 4.0 | $15.00 / $75.00 | $37.50 / $187.50 |
|
||||
|
||||
*The 2.5x multiplier covers infrastructure and API management costs.*
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
**Your Own API Keys** - Use any model at base pricing:
|
||||
|
||||
| Provider | Models | Input / Output |
|
||||
|----------|---------|----------------|
|
||||
| Google | Gemini 2.5 | $0.15 / $0.60 |
|
||||
| Deepseek | V3, R1 | $0.75 / $1.00 |
|
||||
| xAI | Grok 4, Grok 3 | $5.00 / $25.00 |
|
||||
| Groq | Llama 4 Scout | $0.40 / $0.60 |
|
||||
| Cerebras | Llama 3.3 70B | $0.94 / $0.94 |
|
||||
| Ollama | Local models | Free |
|
||||
|
||||
*Pay providers directly with no markup*
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Callout type="warning">
|
||||
Pricing shown reflects rates as of July 14, 2025. Check provider documentation for current pricing.
|
||||
</Callout>
|
||||
|
||||
### Cost Optimization
|
||||
|
||||
<Accordions>
|
||||
<Accordion title="Active Block Indicator">
|
||||
The currently executing block is highlighted in the workflow editor, making it easy to follow
|
||||
the execution flow in real-time. This visual indicator helps you understand exactly where in
|
||||
your workflow the execution is currently happening.
|
||||
<Accordion title="Model Selection">
|
||||
Choose models based on task complexity. Simple tasks can use GPT-4.1-nano ($0.10/$0.40) while complex reasoning might need o1 or Claude Opus.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Live Logs Panel">
|
||||
Execution logs appear in real-time in the logs panel on the right side. These logs include
|
||||
detailed information about each block's execution, including inputs, outputs, execution time, and
|
||||
any errors that occur. You can use these logs to debug your workflow and understand how data flows
|
||||
between blocks.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Block States">
|
||||
Each block's state (pending, executing, completed, or error) is visually indicated in the workflow
|
||||
editor. This helps you quickly identify which blocks have executed successfully and which may have
|
||||
encountered issues.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Performance Metrics">
|
||||
Detailed timing information shows how long each block takes to execute, helping you identify
|
||||
performance bottlenecks in your workflow. The execution engine tracks start time, end time, and
|
||||
total duration for both individual blocks and the entire workflow.
|
||||
|
||||
<Accordion title="Prompt Engineering">
|
||||
Well-structured, concise prompts reduce token usage without sacrificing quality.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Local Models">
|
||||
Use Ollama for non-critical tasks to eliminate API costs entirely.
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
## Performance Optimization
|
||||
## Usage Monitoring
|
||||
|
||||
Optimize your workflows for better performance:
|
||||
Monitor your usage and billing in Settings → Subscription:
|
||||
|
||||
### Block Optimization
|
||||
|
||||
- **Break Down Complex Blocks**: Split complex operations into multiple simpler blocks
|
||||
- **Minimize External Calls**: Batch API requests where possible
|
||||
- **Cache Results**: Use Memory blocks to store and reuse results
|
||||
- **Optimize Function Code**: Write efficient JavaScript/TypeScript code
|
||||
|
||||
### Data Flow Optimization
|
||||
|
||||
- **Filter Data Early**: Process only the data you need as early as possible
|
||||
- **Minimize Data Transfer**: Pass only necessary fields between blocks
|
||||
- **Use Appropriate Data Structures**: Choose efficient data structures for your use case
|
||||
- **Avoid Redundant Computations**: Don't recalculate values that haven't changed
|
||||
|
||||
### Execution Configuration
|
||||
|
||||
- **Set Appropriate Timeouts**: Configure timeouts based on expected execution time
|
||||
- **Limit Parallel Executions**: Control how many workflows can run simultaneously
|
||||
- **Schedule During Off-Peak Hours**: Run resource-intensive workflows when system load is lower
|
||||
- **Monitor Resource Usage**: Keep an eye on memory and CPU usage
|
||||
|
||||
<Callout type="info">
|
||||
Performance optimization is especially important for workflows that run frequently or process
|
||||
large amounts of data.
|
||||
</Callout>
|
||||
|
||||
## Advanced Execution Context
|
||||
|
||||
The execution context maintains detailed information about the workflow execution:
|
||||
|
||||
```javascript
|
||||
// Example of execution context structure (simplified)
|
||||
{
|
||||
// Block states indexed by block ID
|
||||
blockStates: {
|
||||
"block-1": { output: { content: "..." }, status: "completed" },
|
||||
"block-2": { output: { data: { ... } }, status: "completed" },
|
||||
"block-3": { status: "pending" }
|
||||
},
|
||||
|
||||
// Active execution path
|
||||
activeExecutionPath: Set(["block-1", "block-2", "block-5"]),
|
||||
|
||||
// Routing decisions
|
||||
decisions: {
|
||||
router: Map(["router-1" => "block-5"]),
|
||||
condition: Map(["condition-1" => "condition-true"])
|
||||
},
|
||||
|
||||
// Loop iterations
|
||||
loopIterations: Map(["loop-1" => 2]),
|
||||
|
||||
// Environment variables
|
||||
env: { "API_KEY": "...", "MAX_RETRIES": "3" },
|
||||
|
||||
// Execution logs
|
||||
logs: [
|
||||
{ blockId: "block-1", timestamp: "...", status: "completed", duration: 120 },
|
||||
{ blockId: "block-2", timestamp: "...", status: "completed", duration: 85 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This context is used internally by the execution engine but understanding its structure can help you debug complex workflows.
|
||||
|
||||
## Debugging Techniques
|
||||
|
||||
Advanced techniques for debugging workflow execution:
|
||||
|
||||
### Console Logging
|
||||
|
||||
Add strategic console.log statements in Function blocks:
|
||||
|
||||
```javascript
|
||||
console.log('Input to processData:', JSON.stringify(input, null, 2))
|
||||
console.log('Processing step 1 complete:', intermediateResult)
|
||||
console.log('Final result:', finalResult)
|
||||
```
|
||||
|
||||
### State Inspection
|
||||
|
||||
Use Function blocks to inspect the current state:
|
||||
|
||||
```javascript
|
||||
function debugState() {
|
||||
// Log all inputs
|
||||
console.log('All inputs:', input)
|
||||
|
||||
// Return a debug object with relevant information
|
||||
return {
|
||||
debug: true,
|
||||
inputSummary: {
|
||||
hasUserData: !!input.userBlock,
|
||||
apiStatus: input.apiBlock?.status,
|
||||
itemCount: input.dataBlock?.items?.length || 0,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Execution Tracing
|
||||
|
||||
Enable detailed execution tracing for complex workflows:
|
||||
|
||||
1. Add a Memory block to accumulate trace information
|
||||
2. Add trace logging in key Function blocks
|
||||
3. Review the trace after execution to understand the flow
|
||||
|
||||
### Performance Profiling
|
||||
|
||||
Identify performance bottlenecks:
|
||||
|
||||
```javascript
|
||||
function profileOperation() {
|
||||
const start = performance.now()
|
||||
|
||||
// Perform the operation
|
||||
const result = performExpensiveOperation()
|
||||
|
||||
const end = performance.now()
|
||||
console.log(`Operation took ${end - start}ms`)
|
||||
|
||||
return {
|
||||
result,
|
||||
executionTime: end - start,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By mastering these advanced execution features, you can create more robust, efficient, and sophisticated workflows in Sim Studio.
|
||||
- **Current Usage**: Real-time usage and costs for the current period
|
||||
- **Usage Limits**: Plan limits with visual progress indicators
|
||||
- **Billing Details**: Projected charges and minimum commitments
|
||||
- **Plan Management**: Upgrade options and billing history
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { File, Files, Folder } from 'fumadocs-ui/components/files'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
import {
|
||||
AgentIcon,
|
||||
ApiIcon,
|
||||
@@ -14,44 +15,37 @@ import {
|
||||
CodeIcon,
|
||||
ConditionalIcon,
|
||||
ConnectIcon,
|
||||
ResponseIcon,
|
||||
StarterIcon,
|
||||
LoopIcon,
|
||||
ParallelIcon,
|
||||
} from '@/components/icons'
|
||||
|
||||
When you run a workflow in Sim Studio, the execution engine follows a systematic process to ensure blocks are executed in the correct order and data flows properly between them.
|
||||
When you run a workflow in Sim Studio, the execution engine follows a systematic process to ensure blocks are executed in the correct order with proper data flow.
|
||||
|
||||
## Execution Flow
|
||||
|
||||
The execution of a workflow follows these key steps:
|
||||
The execution engine runs workflows in layers, processing blocks based on their dependencies:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Validation Before execution begins, the workflow is validated to ensure it has: - An enabled
|
||||
starter block with no incoming connections - Properly connected blocks with valid configurations
|
||||
- No circular dependencies (except in intentional loops) - Valid input and output types between
|
||||
connected blocks
|
||||
<strong>Validation</strong>: Ensures the workflow has a starter block with no incoming connections and all blocks are properly connected.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Initialization The execution context is created, which includes: - Environment variables for
|
||||
the workflow - Input values from the starter block - Initial state for all blocks - Execution path
|
||||
tracking - Loop iteration counters
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Block Execution Blocks are executed in topological order (based on dependencies): - The system
|
||||
identifies which blocks can be executed next - Inputs for each block are resolved from previous
|
||||
block outputs - Each block is executed by its specialized handler - Outputs are stored in the
|
||||
execution context
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Path Determination As execution progresses, the system determines which paths to follow: -
|
||||
Router and conditional blocks make decisions about execution paths - Only blocks on active paths
|
||||
are executed - The path tracker maintains the current execution state
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Layer-based Execution</strong>: Identifies which blocks can execute next based on completed dependencies and executes them in parallel.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Result Collection After all blocks have executed: - Final outputs are collected - Execution
|
||||
logs are compiled - Performance metrics are calculated - Results are presented in the UI
|
||||
<strong>Path Updates</strong>: Router and Condition blocks update the active execution path, determining which blocks execute next.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
<strong>Iteration Processing</strong>: Loop and Parallel blocks manage iterations and create virtual instances for concurrent execution.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
<strong>Result Collection</strong>: Outputs from the final blocks are collected and returned as the workflow result.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -66,7 +60,7 @@ Different block types have different execution behaviors:
|
||||
<Files>
|
||||
<File
|
||||
name="Starter Block"
|
||||
icon={<ConnectIcon className="h-4 w-4" />}
|
||||
icon={<StarterIcon className="h-4 w-4" />}
|
||||
annotation="Initiates workflow execution and provides initial input values. Every workflow must have exactly one starter block."
|
||||
/>
|
||||
<File
|
||||
@@ -79,6 +73,16 @@ Different block types have different execution behaviors:
|
||||
icon={<ConditionalIcon className="h-4 w-4" />}
|
||||
annotation="Executes different paths based on conditional logic. Evaluates JavaScript expressions to determine which path to follow."
|
||||
/>
|
||||
<File
|
||||
name="Loop Block"
|
||||
icon={<LoopIcon className="h-4 w-4" />}
|
||||
annotation="Executes blocks repeatedly for a fixed number of iterations or over a collection. Manages iteration state and provides access to current item."
|
||||
/>
|
||||
<File
|
||||
name="Parallel Block"
|
||||
icon={<ParallelIcon className="h-4 w-4" />}
|
||||
annotation="Executes blocks concurrently across multiple instances. Distributes work based on count or collection for faster processing."
|
||||
/>
|
||||
</Files>
|
||||
</Card>
|
||||
</Tab>
|
||||
@@ -102,6 +106,11 @@ Different block types have different execution behaviors:
|
||||
icon={<ChartBarIcon className="h-4 w-4" />}
|
||||
annotation="Assesses outputs against defined criteria. Uses AI to evaluate content based on custom metrics."
|
||||
/>
|
||||
<File
|
||||
name="Workflow Block"
|
||||
icon={<ConnectIcon className="h-4 w-4" />}
|
||||
annotation="Execute nested workflows as a single block. Allows modular workflow design by embedding one workflow inside another."
|
||||
/>
|
||||
</Files>
|
||||
</Card>
|
||||
</Tab>
|
||||
@@ -115,6 +124,21 @@ Different block types have different execution behaviors:
|
||||
icon={<ApiIcon className="h-4 w-4" />}
|
||||
annotation="Makes HTTP requests to external services. Configurable with headers, body, and authentication."
|
||||
/>
|
||||
<File
|
||||
name="Knowledge Base Block"
|
||||
icon={
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L3 9V20C3 20.55 3.45 21 4 21H9V14H15V21H20C20.55 21 21 20.55 21 20V9L12 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<circle cx="12" cy="10" r="2" stroke="currentColor" strokeWidth="2"/>
|
||||
</svg>
|
||||
}
|
||||
annotation="Search and interact with knowledge bases. Performs semantic search, retrieves documents, and manages knowledge data."
|
||||
/>
|
||||
<File
|
||||
name="Response Block"
|
||||
icon={<ResponseIcon className="h-4 w-4" />}
|
||||
annotation="Format and return responses from workflows. Configure response data, status codes, and headers."
|
||||
/>
|
||||
<File
|
||||
name="Tool Blocks"
|
||||
icon={<CodeIcon className="h-4 w-4" />}
|
||||
@@ -137,6 +161,14 @@ Run workflows on-demand through the Sim Studio interface by clicking the "Run" b
|
||||
- One-off tasks
|
||||
- Workflows that need human supervision
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/manual-execution-light.png"
|
||||
darkSrc="/static/dark/manual-execution-dark.png"
|
||||
alt="Manual Execution"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
### Scheduled Execution
|
||||
|
||||
Configure workflows to run automatically on a specified schedule:
|
||||
@@ -146,6 +178,14 @@ Configure workflows to run automatically on a specified schedule:
|
||||
- Configure timezone settings
|
||||
- Set minimum and maximum execution intervals
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/scheduled-execution-light.png"
|
||||
darkSrc="/static/dark/scheduled-execution-dark.png"
|
||||
alt="Scheduled Execution"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
### API Endpoints
|
||||
|
||||
Each workflow can be exposed as an API endpoint:
|
||||
@@ -155,6 +195,14 @@ Each workflow can be exposed as an API endpoint:
|
||||
- Send custom inputs via POST requests
|
||||
- Receive execution results as JSON responses
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/api-execution-light.png"
|
||||
darkSrc="/static/dark/api-execution-dark.png"
|
||||
alt="API Execution"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
### Webhooks
|
||||
|
||||
Configure workflows to execute in response to external events:
|
||||
@@ -164,6 +212,14 @@ Configure workflows to execute in response to external events:
|
||||
- Configure webhook security settings
|
||||
- Support for specialized webhooks (GitHub, Stripe, etc.)
|
||||
|
||||
<ThemeImage
|
||||
lightSrc="/static/light/webhook-execution-light.png"
|
||||
darkSrc="/static/dark/webhook-execution-dark.png"
|
||||
alt="Webhook Execution"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
<Callout type="info">
|
||||
The execution method you choose depends on your workflow's purpose. Manual execution is great for
|
||||
development, while scheduled execution, API endpoints, and webhooks are better for production use
|
||||
@@ -172,29 +228,21 @@ Configure workflows to execute in response to external events:
|
||||
|
||||
## Execution Context
|
||||
|
||||
Each workflow execution maintains a detailed context that includes:
|
||||
Each workflow execution maintains a context that tracks:
|
||||
|
||||
- **Block States**: Outputs and execution status of each block
|
||||
- **Execution Path**: The active path through the workflow
|
||||
- **Routing Decisions**: Records of which paths were selected
|
||||
- **Environment Variables**: Configuration values for the workflow
|
||||
- **Execution Logs**: Detailed records of each step in the execution
|
||||
- **Execution Path**: Active blocks based on routing decisions
|
||||
- **Loop/Parallel State**: Current iterations and distribution items
|
||||
- **Environment Variables**: Configuration values available during execution
|
||||
- **Execution Logs**: Detailed records of each block's execution
|
||||
|
||||
This context is maintained throughout the execution and is used to:
|
||||
## Real-Time Monitoring
|
||||
|
||||
- Resolve inputs for blocks
|
||||
- Determine which blocks to execute next
|
||||
- Track the progress of execution
|
||||
- Provide debugging information
|
||||
- Store intermediate results
|
||||
Monitor your workflow execution in real-time:
|
||||
|
||||
## Real-Time Execution Monitoring
|
||||
- **Active Block Highlighting**: Currently executing blocks pulse with animation
|
||||
- **Live Logs**: Execution logs appear instantly in the logs panel
|
||||
- **Block States**: Visual indicators show success, error, or pending states
|
||||
- **Performance Metrics**: Execution time for each block
|
||||
|
||||
As your workflow executes, you can monitor its progress in real-time:
|
||||
|
||||
- **Active Block Highlighting**: The currently executing block is highlighted
|
||||
- **Live Logs**: Execution logs appear in real-time in the logs panel
|
||||
- **Block States**: Visual indicators show each block's execution state
|
||||
- **Performance Metrics**: Timing information for each block's execution
|
||||
|
||||
These monitoring features help you understand how your workflow is executing and identify any issues that arise.
|
||||
These monitoring features help you understand workflow behavior and quickly identify any issues.
|
||||
|
||||
@@ -3,39 +3,16 @@ title: Execution
|
||||
description: Understand how workflows are executed in Sim Studio
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { File, Files, Folder } from 'fumadocs-ui/components/files'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import {
|
||||
AgentIcon,
|
||||
ApiIcon,
|
||||
ChartBarIcon,
|
||||
CodeIcon,
|
||||
ConditionalIcon,
|
||||
ConnectIcon,
|
||||
ExaAIIcon,
|
||||
FirecrawlIcon,
|
||||
GmailIcon,
|
||||
NotionIcon,
|
||||
PerplexityIcon,
|
||||
SlackIcon,
|
||||
} from '@/components/icons'
|
||||
|
||||
Sim Studio provides a powerful execution engine that brings your workflows to life. Understanding how execution works will help you design more effective workflows and troubleshoot any issues that arise.
|
||||
|
||||
<div>
|
||||
<video autoPlay loop muted playsInline className="w-full" src="/loops.mp4"></video>
|
||||
</div>
|
||||
Sim Studio's execution engine brings your workflows to life by processing blocks in the correct order, managing data flow, and handling errors gracefully.
|
||||
|
||||
<Callout type="info">
|
||||
The execution engine handles everything from block execution order to data flow, error handling,
|
||||
and loop management. It ensures your workflows run efficiently and predictably.
|
||||
Every workflow execution follows a deterministic path based on your block connections and logic, ensuring predictable and reliable results.
|
||||
</Callout>
|
||||
|
||||
## Execution Documentation
|
||||
## Documentation Overview
|
||||
|
||||
<Cards>
|
||||
<Card title="Execution Basics" href="/execution/basics">
|
||||
@@ -43,223 +20,109 @@ Sim Studio provides a powerful execution engine that brings your workflows to li
|
||||
workflow
|
||||
</Card>
|
||||
|
||||
<Card title="Loops" href="/execution/loops">
|
||||
Master the powerful loop functionality to create iterative processes and feedback mechanisms
|
||||
</Card>
|
||||
|
||||
<Card title="Advanced Features" href="/execution/advanced">
|
||||
Discover advanced capabilities like error handling, environment variables, and performance
|
||||
optimization
|
||||
<Card title="Logging and Cost Calculation" href="/execution/advanced">
|
||||
Understand workflow logs and how execution costs are calculated in Sim Studio
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## Key Execution Concepts
|
||||
## Key Concepts
|
||||
|
||||
- **Topological Execution** - Blocks are executed in dependency order, ensuring data flows correctly
|
||||
- **Path Tracking** - The system tracks active execution paths based on routing decisions
|
||||
- **Loop Management** - Sophisticated loop handling allows for iterative processing with safeguards
|
||||
- **Real-time Monitoring** - Watch your workflow execute with detailed logs and visual indicators
|
||||
- **Error Handling** - Robust error management keeps your workflows resilient
|
||||
### Topological Execution
|
||||
Blocks execute in dependency order, similar to how a spreadsheet recalculates cells. The execution engine automatically determines which blocks can run based on completed dependencies.
|
||||
|
||||
Whether you're building simple automations or complex AI workflows, understanding execution is key to creating effective solutions in Sim Studio.
|
||||
### Path Tracking
|
||||
The engine actively tracks execution paths through your workflow. Router and Condition blocks dynamically update these paths, ensuring only relevant blocks execute.
|
||||
|
||||
## Execution Flow
|
||||
### Layer-Based Processing
|
||||
Instead of executing blocks one-by-one, the engine identifies layers of blocks that can run in parallel, optimizing performance for complex workflows.
|
||||
|
||||
When you execute a workflow in Sim Studio, the system follows a predictable pattern:
|
||||
### Execution Context
|
||||
Each workflow maintains a rich context during execution containing:
|
||||
- Block outputs and states
|
||||
- Active execution paths
|
||||
- Loop and parallel iteration tracking
|
||||
- Environment variables
|
||||
- Routing decisions
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Validation The workflow is validated to ensure it has an enabled starter block and proper
|
||||
connections. This includes checking that: - The starter block has no incoming connections - All
|
||||
required blocks are present and properly connected - Loop configurations are valid with
|
||||
appropriate iteration limits
|
||||
</Step>
|
||||
## Execution Triggers
|
||||
|
||||
<Step>
|
||||
### Initialization The execution context is created with environment variables and input values.
|
||||
This context maintains the state of the workflow throughout execution, including: - Block outputs
|
||||
and states - Execution path tracking - Routing decisions - Loop iteration counters
|
||||
</Step>
|
||||
Workflows can be executed through multiple channels:
|
||||
|
||||
<Step>
|
||||
### Block Execution Blocks are executed in topological order, with each block's outputs feeding
|
||||
into subsequent blocks. The executor: - Determines the next layer of blocks to execute based on
|
||||
dependencies - Resolves inputs for each block from previous outputs - Dispatches execution to
|
||||
specialized handlers for each block type
|
||||
</Step>
|
||||
- **Manual**: Test and debug directly in the editor
|
||||
- **Deploy as API**: Create an HTTP endpoint secured with API keys
|
||||
- **Deploy as Chat**: Create a conversational interface on a custom subdomain
|
||||
- **Webhooks**: Respond to external events from third-party services
|
||||
- **Scheduled**: Run on a recurring schedule using cron expressions
|
||||
|
||||
<Step>
|
||||
### Path Determination Router and conditional blocks make routing decisions that determine which
|
||||
execution paths to follow. The path tracker: - Updates the active execution path based on these
|
||||
decisions - Ensures that only blocks on active paths are executed - Handles complex branching
|
||||
logic in your workflow
|
||||
</Step>
|
||||
### Deploy as API
|
||||
|
||||
<Step>
|
||||
### Result Collection The final output and execution logs are collected and presented in the UI.
|
||||
You'll see: - Complete execution logs for each block - Performance metrics and timing
|
||||
information - Any errors that occurred during execution - The final workflow output
|
||||
</Step>
|
||||
</Steps>
|
||||
When you deploy a workflow as an API, Sim Studio:
|
||||
- Creates a unique HTTP endpoint: `https://simstudio.ai/api/workflows/{workflowId}/execute`
|
||||
- Generates an API key for authentication
|
||||
- Accepts POST requests with JSON payloads
|
||||
- Returns workflow execution results as JSON
|
||||
|
||||
## Block Types
|
||||
Example API call:
|
||||
```bash
|
||||
curl -X POST https://simstudio.ai/api/workflows/your-workflow-id/execute \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"input": "your data here"}'
|
||||
```
|
||||
|
||||
Sim Studio has two main categories of blocks in workflows:
|
||||
### Deploy as Chat
|
||||
|
||||
<Tabs items={['Orchestration Blocks', 'Output Blocks']}>
|
||||
<Tab value="Orchestration Blocks">
|
||||
<Card>
|
||||
Orchestration blocks control the flow of execution through your workflow.
|
||||
<Files>
|
||||
<File
|
||||
name="Router Blocks"
|
||||
icon={<ConnectIcon className="h-4 w-4" />}
|
||||
annotation="Direct the workflow along specific paths based on dynamic decisions. The router evaluates inputs and selects one of multiple possible paths."
|
||||
/>
|
||||
<File
|
||||
name="Conditional Blocks"
|
||||
icon={<ConditionalIcon className="h-4 w-4" />}
|
||||
annotation="Execute different paths based on conditional logic. Conditions are evaluated to true or false, determining which path to follow."
|
||||
/>
|
||||
</Files>
|
||||
</Card>
|
||||
</Tab>
|
||||
Chat deployment creates a conversational interface for your workflow:
|
||||
- Hosted on a custom subdomain: `https://your-name.simstudio.ai`
|
||||
- Optional authentication (public, password, or email-based)
|
||||
- Customizable UI with your branding
|
||||
- Streaming responses for real-time interaction
|
||||
- Perfect for AI assistants, support bots, or interactive tools
|
||||
|
||||
<Tab value="Output Blocks">
|
||||
<Card>
|
||||
Output blocks perform operations and generate results that can be used by downstream blocks.
|
||||
<Files>
|
||||
<File
|
||||
name="Agent Block"
|
||||
icon={<AgentIcon className="h-4 w-4" />}
|
||||
annotation="Interact with AI models to generate content. Supports various LLM providers with optional tool calling capabilities."
|
||||
/>
|
||||
<File
|
||||
name="Function Blocks"
|
||||
icon={<CodeIcon className="h-4 w-4" />}
|
||||
annotation="Execute custom JavaScript/TypeScript code to process data. Runs in a secure sandbox environment with appropriate timeout limits."
|
||||
/>
|
||||
<File
|
||||
name="API Blocks"
|
||||
icon={<ApiIcon className="h-4 w-4" />}
|
||||
annotation="Make HTTP requests to external services. Configure headers, body, and authentication for REST API interactions."
|
||||
/>
|
||||
<File
|
||||
name="Evaluator Blocks"
|
||||
icon={<ChartBarIcon className="h-4 w-4" />}
|
||||
annotation="Assess outputs against defined criteria with customizable scoring logic."
|
||||
/>
|
||||
</Files>
|
||||
</Card>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
Each deployment method passes data to your workflow's starter block, beginning the execution flow.
|
||||
|
||||
## Real-Time Monitoring
|
||||
## Programmatic Execution
|
||||
|
||||
As your workflow executes, Sim Studio provides powerful real-time monitoring capabilities:
|
||||
Execute workflows from your applications using our official SDKs:
|
||||
|
||||
<Accordions>
|
||||
<Accordion title="Active Block Indicator">
|
||||
The currently executing block is highlighted in the workflow editor, making it easy to follow
|
||||
the execution flow in real-time. This visual indicator helps you understand exactly where in
|
||||
your workflow the execution is currently happening.
|
||||
</Accordion>
|
||||
```bash
|
||||
# TypeScript/JavaScript
|
||||
npm install simstudio-ts-sdk
|
||||
|
||||
<Accordion title="Live Logs Panel">
|
||||
Execution logs appear in real-time in the logs panel on the right side. These logs include
|
||||
detailed information about each block's execution, including inputs, outputs, execution time, and
|
||||
any errors that occur. You can use these logs to debug your workflow and understand how data flows
|
||||
between blocks.
|
||||
</Accordion>
|
||||
# Python
|
||||
pip install simstudio-sdk
|
||||
```
|
||||
|
||||
<Accordion title="Block States">
|
||||
Each block's state (pending, executing, completed, or error) is visually indicated in the workflow
|
||||
editor. This helps you quickly identify which blocks have executed successfully and which may have
|
||||
encountered issues.
|
||||
</Accordion>
|
||||
```typescript
|
||||
// TypeScript Example
|
||||
import { SimStudioClient } from 'simstudio-ts-sdk';
|
||||
|
||||
<Accordion title="Performance Metrics">
|
||||
Detailed timing information shows how long each block takes to execute, helping you identify
|
||||
performance bottlenecks in your workflow. The execution engine tracks start time, end time, and
|
||||
total duration for both individual blocks and the entire workflow.
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
const client = new SimStudioClient({
|
||||
apiKey: 'your-api-key'
|
||||
});
|
||||
|
||||
## Execution Methods
|
||||
const result = await client.executeWorkflow('workflow-id', {
|
||||
input: { message: 'Hello' }
|
||||
});
|
||||
```
|
||||
|
||||
Sim Studio offers multiple ways to trigger workflow execution:
|
||||
## Best Practices
|
||||
|
||||
<Cards>
|
||||
<Card title="Manual Execution" href="#">
|
||||
Run workflows on-demand through the Sim Studio interface. This is perfect for testing and
|
||||
development, allowing you to iteratively refine your workflow with immediate feedback.
|
||||
</Card>
|
||||
### Design for Reliability
|
||||
- Handle errors gracefully with appropriate fallback paths
|
||||
- Use environment variables for sensitive data
|
||||
- Add logging to Function blocks for debugging
|
||||
|
||||
<Card title="Scheduled Execution" href="#">
|
||||
Configure workflows to run automatically on a specified schedule using cron expressions. Ideal for
|
||||
regular data processing, reporting tasks, or any workflow that needs to run periodically without
|
||||
manual intervention.
|
||||
</Card>
|
||||
### Optimize Performance
|
||||
- Minimize external API calls where possible
|
||||
- Use parallel execution for independent operations
|
||||
- Cache results with Memory blocks when appropriate
|
||||
|
||||
<Card title="API Endpoints" href="#">
|
||||
Each workflow can be exposed as an API endpoint with authentication, allowing external systems to
|
||||
trigger execution with custom inputs. This enables seamless integration with your existing
|
||||
applications and services.
|
||||
</Card>
|
||||
### Monitor Executions
|
||||
- Review logs regularly to understand performance patterns
|
||||
- Track costs for AI model usage
|
||||
- Use workflow snapshots to debug issues
|
||||
|
||||
<Card title="Webhooks" href="#">
|
||||
Configure workflows to execute in response to external events via webhook triggers. This allows
|
||||
your workflows to react to events from third-party services like GitHub, Stripe, or any platform
|
||||
that supports webhooks.
|
||||
</Card>
|
||||
</Cards>
|
||||
## What's Next?
|
||||
|
||||
## Advanced Execution Features
|
||||
|
||||
### Loops
|
||||
|
||||
Sim Studio supports sophisticated loop constructs, allowing parts of your workflow to execute repeatedly:
|
||||
|
||||
- **Iteration Limits** - Configure maximum iterations (default: 5) to prevent infinite loops and minimum iterations to ensure the loop executes a certain number of times.
|
||||
- **Conditional Looping** - Continue looping until specific conditions are met, with the loop manager tracking iteration counts.
|
||||
- **Loop Reset** - Automatically resets block states between iterations, allowing blocks to be re-executed with updated inputs.
|
||||
- **Feedback Paths** - Create feedback loops where outputs from later blocks feed back into earlier blocks in the workflow.
|
||||
|
||||
### Error Handling
|
||||
|
||||
The execution engine includes built-in error handling mechanisms:
|
||||
|
||||
- **Block-Level Errors** - Errors in one block don't necessarily stop the entire workflow execution.
|
||||
- **Detailed Error Logs** - Comprehensive error information is captured in the execution logs, including error messages, stack traces, and relevant context.
|
||||
- **Fallback Mechanisms** - For function execution, the system tries Freestyle execution first, then falls back to VM execution if needed.
|
||||
- **Recovery Options** - Configure blocks to retry on failure or implement custom error handling logic.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Use environment variables to securely store and access sensitive information:
|
||||
|
||||
- **API Keys** - Store API credentials securely without hardcoding them in your workflow.
|
||||
- **Configuration Values** - Manage environment-specific configuration values.
|
||||
- **Runtime Availability** - All environment variables are available to blocks during execution.
|
||||
- **Secure Storage** - Values are encrypted at rest and only decrypted during execution.
|
||||
|
||||
## Execution Context
|
||||
|
||||
Each workflow execution maintains a detailed context that includes:
|
||||
|
||||
- **Block States** - Outputs and execution status of each block, indexed by block ID.
|
||||
- **Execution Path** - The active path through the workflow based on routing decisions.
|
||||
- **Routing Decisions** - Records which paths were selected by router and conditional blocks.
|
||||
- **Loop Iterations** - Tracks current iteration count for each loop in the workflow.
|
||||
- **Environment Variables** - Configuration values available to all blocks during execution.
|
||||
- **Execution Logs** - A chronological record of block executions with timing information.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Block Complexity** - Complex blocks with heavy computation may take longer to execute. Consider breaking complex operations into multiple blocks.
|
||||
- **External Dependencies** - Blocks that rely on external services may be affected by network latency or service availability.
|
||||
- **Execution Layers** - The executor processes blocks in layers based on dependencies, which can affect overall execution time.
|
||||
- **Code Optimization** - For function blocks, optimize your code to reduce execution time and resource usage.
|
||||
- **Timeout Limits** - Function blocks have configurable timeout limits to prevent long-running operations from blocking execution.
|
||||
|
||||
By understanding these execution principles, you can design more efficient and effective workflows in Sim Studio.
|
||||
Start with [Execution Basics](/execution/basics) to understand how workflows run, then explore [Logging and Cost Calculation](/execution/advanced) to monitor and optimize your executions.
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
---
|
||||
title: Loops
|
||||
description: Creating iterative processes with loops in Sim Studio
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
|
||||
Loops are a powerful feature in Sim Studio that allow you to create iterative processes, implement feedback mechanisms, and build more sophisticated workflows.
|
||||
|
||||
<div>
|
||||
<video autoPlay loop muted playsInline className="w-full" src="/loops.mp4"></video>
|
||||
</div>
|
||||
|
||||
## What Are Loops?
|
||||
|
||||
Loops in Sim Studio allow a group of blocks to execute repeatedly, with each iteration building on the results of the previous one. This enables:
|
||||
|
||||
- **Iterative Refinement**: Progressively improve outputs through multiple passes
|
||||
- **Batch Processing**: Process collections of items one at a time
|
||||
- **Feedback Mechanisms**: Create systems that learn from their own outputs
|
||||
- **Conditional Processing**: Continue execution until specific criteria are met
|
||||
|
||||
<Callout type="info">
|
||||
Loops are particularly powerful for AI workflows, allowing you to implement techniques like
|
||||
chain-of-thought reasoning, recursive refinement, and multi-step problem solving.
|
||||
</Callout>
|
||||
|
||||
## Creating Loops
|
||||
|
||||
To create a loop in your workflow:
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
<strong>Select Blocks</strong>: Choose the blocks you want to include in the loop
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Create Loop</strong>: Use the "Create Loop" option in the editor
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Configure Loop Settings</strong>: Set iteration limits and conditions
|
||||
</Step>
|
||||
<Step>
|
||||
<strong>Create Feedback Connections</strong>: Connect outputs from later blocks back to earlier
|
||||
blocks
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Loop Configuration
|
||||
|
||||
When configuring a loop, you can set several important parameters:
|
||||
|
||||
### Iteration Limits
|
||||
|
||||
- **Maximum Iterations**: The maximum number of times the loop can execute (default: 5)
|
||||
- **Minimum Iterations**: The minimum number of times the loop must execute before checking conditions
|
||||
|
||||
<Callout type="warning">
|
||||
Always set a reasonable maximum iteration limit to prevent infinite loops. The default limit of 5
|
||||
iterations is a good starting point for most workflows.
|
||||
</Callout>
|
||||
|
||||
### Loop Conditions
|
||||
|
||||
Loops can continue based on different types of conditions:
|
||||
|
||||
<Tabs items={['Conditional Block', 'Function Block', 'Fixed Iterations']}>
|
||||
<Tab>
|
||||
Use a Condition block to determine whether the loop should continue:
|
||||
|
||||
```javascript
|
||||
// Example condition in a Condition block
|
||||
function shouldContinueLoop() {
|
||||
// Get the current score from an evaluator block
|
||||
const score = input.evaluatorBlock.score;
|
||||
|
||||
// Continue looping if score is below threshold
|
||||
return score < 0.8;
|
||||
}
|
||||
```
|
||||
|
||||
The loop will continue executing as long as the condition returns true and the maximum iteration limit hasn't been reached.
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
Use a Function block to implement complex loop conditions:
|
||||
|
||||
```javascript
|
||||
// Example condition in a Function block
|
||||
function processAndCheckContinuation() {
|
||||
// Process data from previous blocks
|
||||
const currentResult = input.agentBlock.content;
|
||||
const previousResults = input.memoryBlock.results || [];
|
||||
|
||||
// Store results for comparison
|
||||
const allResults = [...previousResults, currentResult];
|
||||
|
||||
// Check if we've converged (results not changing significantly)
|
||||
const shouldContinue = previousResults.length === 0 ||
|
||||
currentResult !== previousResults[previousResults.length - 1];
|
||||
|
||||
return {
|
||||
results: allResults,
|
||||
shouldContinue: shouldContinue
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Connect this Function block's output to a Condition block to control the loop.
|
||||
</Tab>
|
||||
|
||||
<Tab>
|
||||
Execute the loop for a fixed number of iterations:
|
||||
|
||||
```
|
||||
// Set in loop configuration
|
||||
Minimum Iterations: 3
|
||||
Maximum Iterations: 3
|
||||
```
|
||||
|
||||
This will run the loop exactly 3 times, regardless of any conditions.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Loop Execution
|
||||
|
||||
When a workflow with loops executes, the loop manager handles the iteration process:
|
||||
|
||||
1. **First Pass**: All blocks in the loop execute normally
|
||||
2. **Iteration Check**: The system checks if another iteration should occur
|
||||
3. **State Reset**: If continuing, block states within the loop are reset
|
||||
4. **Next Iteration**: The loop blocks execute again with updated inputs
|
||||
5. **Termination**: The loop stops when either:
|
||||
- The maximum iteration count is reached
|
||||
- A loop condition evaluates to false (after minimum iterations)
|
||||
|
||||
## Loop Use Cases
|
||||
|
||||
Loops enable powerful workflow patterns:
|
||||
|
||||
### Iterative Refinement
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Example: Content Refinement</h4>
|
||||
<div className="mb-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Create a loop where an Agent block generates content, an Evaluator block assesses it, and a
|
||||
Function block decides whether to continue refining.
|
||||
</div>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Agent generates initial content</li>
|
||||
<li>Evaluator scores the content</li>
|
||||
<li>Function analyzes score and provides feedback</li>
|
||||
<li>Loop back to Agent with feedback for improvement</li>
|
||||
<li>Continue until quality threshold is reached</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Batch Processing
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Example: Data Processing Pipeline</h4>
|
||||
<div className="mb-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Process a collection of items one at a time through a series of blocks.
|
||||
</div>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Function block extracts the next item from a collection</li>
|
||||
<li>Processing blocks operate on the single item</li>
|
||||
<li>Results are accumulated in a Memory block</li>
|
||||
<li>Loop continues until all items are processed</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
### Recursive Problem Solving
|
||||
|
||||
<div className="mb-4 rounded-md border p-4">
|
||||
<h4 className="font-medium">Example: Multi-step Reasoning</h4>
|
||||
<div className="mb-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Implement a recursive approach to complex problem solving.
|
||||
</div>
|
||||
<ol className="list-decimal pl-5 text-sm">
|
||||
<li>Agent analyzes the current problem state</li>
|
||||
<li>Function block implements a step in the solution</li>
|
||||
<li>Condition block checks if the problem is solved</li>
|
||||
<li>Loop continues until solution is found or maximum steps reached</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
## Best Practices for Loops
|
||||
|
||||
To use loops effectively in your workflows:
|
||||
|
||||
- **Set Appropriate Limits**: Always configure reasonable iteration limits
|
||||
- **Use Memory Blocks**: Store state between iterations with Memory blocks
|
||||
- **Include Exit Conditions**: Define clear conditions for when loops should terminate
|
||||
- **Monitor Performance**: Watch for performance impacts with many iterations
|
||||
- **Test Thoroughly**: Verify that loops terminate as expected in all scenarios
|
||||
|
||||
<Callout type="warning">
|
||||
Loops with many blocks or complex operations can impact performance. Consider optimizing
|
||||
individual blocks if your loops need many iterations.
|
||||
</Callout>
|
||||
|
||||
## Loop Debugging
|
||||
|
||||
When debugging loops in your workflows:
|
||||
|
||||
- **Check Iteration Counts**: Verify the loop is executing the expected number of times
|
||||
- **Inspect Block Inputs/Outputs**: Look at how data changes between iterations
|
||||
- **Review Loop Conditions**: Ensure conditions are evaluating as expected
|
||||
- **Use Console Logging**: Add console.log statements in Function blocks to track loop progress
|
||||
- **Monitor Memory Usage**: Watch for growing data structures that might cause performance issues
|
||||
|
||||
By mastering loops, you can create much more sophisticated and powerful workflows in Sim Studio.
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Execution",
|
||||
"pages": ["basics", "loops", "advanced"]
|
||||
"pages": ["basics", "advanced"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Getting Started
|
||||
description: Build, test, and optimize your agentic workflows
|
||||
description: Build your first AI workflow in 5 minutes
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
@@ -8,6 +8,7 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { File, Files, Folder } from 'fumadocs-ui/components/files'
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps'
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
|
||||
import { ThemeImage } from '@/components/ui/theme-image'
|
||||
import {
|
||||
AgentIcon,
|
||||
ApiIcon,
|
||||
@@ -23,79 +24,171 @@ import {
|
||||
SlackIcon,
|
||||
} from '@/components/icons'
|
||||
|
||||
Sim Studio is a powerful, user-friendly platform for building, testing, and optimizing your agentic workflows. This documentation will help you understand how to use the various components of Sim Studio to create sophisticated agent-based applications.
|
||||
This tutorial will guide you through building your first AI workflow in Sim Studio. We'll create a people research agent that can find information about individuals using state-of-the-art LLM-Search tools.
|
||||
|
||||
<Callout type="info">
|
||||
This guide will walk you through the essential concepts and help you get started building your
|
||||
first workflow.
|
||||
This tutorial takes about 10 minutes and covers the essential concepts of building workflows in Sim Studio.
|
||||
</Callout>
|
||||
|
||||
## Core Components
|
||||
## What We're Building
|
||||
|
||||
Sim Studio is built around two primary components:
|
||||
A people research agent that:
|
||||
1. Receives a person's name via chat interface
|
||||
2. Uses an AI agent with advanced search capabilities
|
||||
3. Searches the web using state-of-the-art LLM-Search tools (Exa and Linkup)
|
||||
4. Extracts structured information using a response format
|
||||
5. Returns comprehensive data about the person
|
||||
|
||||
### Blocks
|
||||
<ThemeImage
|
||||
lightSrc="/static/examples/started/started-1.png"
|
||||
darkSrc="/static/examples/started/started-1.png"
|
||||
alt="Getting Started Example"
|
||||
width={800}
|
||||
height={500}
|
||||
/>
|
||||
|
||||
Blocks are the fundamental building elements of your workflows. Each block serves a specific purpose:
|
||||
## Step-by-Step Tutorial
|
||||
|
||||
<Steps>
|
||||
<Step title="Create workflow and add AI agent">
|
||||
Open Sim Studio and click "New Workflow" in the dashboard. Name it "Getting Started".
|
||||
|
||||
When you create a new workflow, it automatically includes a **Start block** - this is the entry point that receives input from users. For this example, we'll be triggering the workflow via chat, so we don't need to configure anything on the Start block.
|
||||
|
||||
Now drag an **Agent Block** onto the canvas from the blocks panel on the left.
|
||||
|
||||
Configure the Agent Block:
|
||||
- **Model**: Select "OpenAI GPT-4o"
|
||||
- **System Prompt**: "You are a people research agent. When given a person's name, use your available search tools to find comprehensive information about them including their location, profession, educational background, and other relevant details."
|
||||
- **User Prompt**: Drag the connection from the Start block's output into this field (this connects `<start.input>` to the user prompt)
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/static/examples/started/started-2.mp4"></video>
|
||||
</div>
|
||||
</Step>
|
||||
|
||||
<Step title="Add tools to the agent">
|
||||
Let's enhance our agent with tools for better capabilities. Click on the Agent block to select it.
|
||||
|
||||
In the **Tools** section:
|
||||
- Click **Add Tool**
|
||||
- Select **Exa** from the available tools
|
||||
- Select **Linkup** from the available tools
|
||||
- Add your API keys for both tools (this allows the agent to search the web and access additional information)
|
||||
|
||||
<div className="mx-auto w-3/5 overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/static/examples/started/started-3.mp4"></video>
|
||||
</div>
|
||||
</Step>
|
||||
|
||||
<Step title="Test the basic workflow">
|
||||
Now let's test our workflow. Go to the **Chat panel** on the right side of the screen.
|
||||
|
||||
In the chat panel:
|
||||
- Click the dropdown and select `agent1.content` (this will show us the output of our agent)
|
||||
- Enter a test message like: "John is a software engineer from San Francisco who studied Computer Science at Stanford University."
|
||||
- Click "Send" to run the workflow
|
||||
|
||||
You should see the agent's response analyzing the person described in your text.
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/static/examples/started/started-4.mp4"></video>
|
||||
</div>
|
||||
</Step>
|
||||
|
||||
<Step title="Add structured output">
|
||||
Now let's make our agent return structured data. Click on the Agent block to select it.
|
||||
|
||||
In the **Response Format** section:
|
||||
- Click the **magic wand icon** (✨) next to the schema field
|
||||
- In the prompt that appears, type: "create a schema named person, that contains location, profession, and education"
|
||||
- The AI will generate a JSON schema for you automatically
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/static/examples/started/started-5.mp4"></video>
|
||||
</div>
|
||||
</Step>
|
||||
|
||||
<Step title="Test the structured output">
|
||||
Go back to the **Chat panel**.
|
||||
|
||||
Since we added a response format, new output options are now available:
|
||||
- Click the dropdown and select the new structured output option (the schema we just created)
|
||||
- Enter a new test message like: "Sarah is a marketing manager from New York who has an MBA from Harvard Business School."
|
||||
- Click "Send" to run the workflow again
|
||||
|
||||
You should now see structured JSON output with the person's information organized into location, profession, and education fields.
|
||||
|
||||
<div className="mx-auto w-full overflow-hidden rounded-lg">
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/static/examples/started/started-6.mp4"></video>
|
||||
</div>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## What You Just Built
|
||||
|
||||
Congratulations! You've created your first AI workflow that:
|
||||
- ✅ Receives text input via chat interface
|
||||
- ✅ Uses AI to extract information from unstructured text
|
||||
- ✅ Integrates external tools (Exa and Linkup) for enhanced capabilities
|
||||
- ✅ Returns structured JSON data using AI-generated schemas
|
||||
- ✅ Demonstrates workflow testing and iteration
|
||||
- ✅ Shows the power of visual workflow building
|
||||
|
||||
## Key Concepts You Learned
|
||||
|
||||
### Block Types Used
|
||||
|
||||
<Files>
|
||||
<File
|
||||
name="Start Block"
|
||||
icon={<ConnectIcon className="h-4 w-4" />}
|
||||
annotation="Entry point for user input (auto-included)"
|
||||
/>
|
||||
<File
|
||||
name="Agent Block"
|
||||
icon={<AgentIcon className="h-4 w-4" />}
|
||||
annotation="Create AI agents using any LLM provider"
|
||||
/>
|
||||
<File
|
||||
name="API Block"
|
||||
icon={<ApiIcon className="h-4 w-4" />}
|
||||
annotation="Connect to external services and APIs"
|
||||
/>
|
||||
<File
|
||||
name="Condition Block"
|
||||
icon={<ConditionalIcon className="h-4 w-4" />}
|
||||
annotation="Add conditional branching to your workflows"
|
||||
/>
|
||||
<File
|
||||
name="Function Block"
|
||||
icon={<CodeIcon className="h-4 w-4" />}
|
||||
annotation="Execute custom JavaScript/TypeScript code"
|
||||
/>
|
||||
<File
|
||||
name="Evaluator Block"
|
||||
icon={<ChartBarIcon className="h-4 w-4" />}
|
||||
annotation="Assess responses against defined criteria"
|
||||
/>
|
||||
<File
|
||||
name="Router Block"
|
||||
icon={<ConnectIcon className="h-4 w-4" />}
|
||||
annotation="Direct workflow execution based on input analysis"
|
||||
annotation="AI model for text processing and analysis"
|
||||
/>
|
||||
</Files>
|
||||
|
||||
### Tools
|
||||
### Core Workflow Concepts
|
||||
|
||||
Tools extend the capabilities of agents. They provide additional functionality for agents by enabling you to interface with your favorite data sources and take action (e.g posting on X, sending an email)
|
||||
**Data Flow**: Variables flow between blocks by dragging connections
|
||||
|
||||
<Files>
|
||||
<File name="Gmail Tool" icon={<GmailIcon className="h-4 w-4" />} />
|
||||
<File name="Firecrawl Tool" icon={<FirecrawlIcon className="h-4 w-4" />} />
|
||||
<File name="Perplexity Tool" icon={<PerplexityIcon className="h-4 w-4" />} />
|
||||
<File name="Notion Tool" icon={<NotionIcon className="h-4 w-4" />} />
|
||||
<File name="Exa AI Tool" icon={<ExaAIIcon className="h-4 w-4" />} />
|
||||
<File name="Slack Tool" icon={<SlackIcon className="h-4 w-4" />} />
|
||||
</Files>
|
||||
**Chat Interface**: Test workflows in real-time using the chat panel with different output options
|
||||
|
||||
## Getting Started
|
||||
**Tool Integration**: Enhance agent capabilities by adding external tools like Exa and Linkup
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a new workflow">
|
||||
Start by creating a new workflow in the Sim Studio dashboard.
|
||||
</Step>
|
||||
<Step title="Add your first block">Drag and drop a block from the sidebar onto the canvas.</Step>
|
||||
<Step title="Configure the block">
|
||||
Set up the block's parameters and inputs according to your needs.
|
||||
</Step>
|
||||
<Step title="Connect blocks">
|
||||
Create connections between blocks to define the flow of data and execution.
|
||||
</Step>
|
||||
<Step title="Test your workflow">Run your workflow with test inputs to verify its behavior.</Step>
|
||||
</Steps>
|
||||
**Variable References**: Access block outputs using `<blockName.output>` syntax
|
||||
|
||||
**Structured Output**: Use JSON schemas to get consistent, structured data from AI
|
||||
|
||||
**AI-Generated Schemas**: Use the magic wand (✨) to generate schemas with natural language
|
||||
|
||||
**Iterative Development**: Test, modify, and re-test workflows easily
|
||||
|
||||
## Next Steps
|
||||
|
||||
<Cards>
|
||||
<Card title="Add More Blocks" href="/blocks">
|
||||
Learn about API, Function, and Condition blocks
|
||||
</Card>
|
||||
<Card title="Use Tools" href="/tools">
|
||||
Integrate with external services like Gmail, Slack, and Notion
|
||||
</Card>
|
||||
<Card title="Add Custom Logic" href="/blocks/function">
|
||||
Use Function blocks for custom data processing
|
||||
</Card>
|
||||
<Card title="Deploy Your Workflow" href="/execution">
|
||||
Make your workflow accessible via REST API
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## Need Help?
|
||||
|
||||
**Stuck on a step?** Check our [Blocks documentation](/blocks) for detailed explanations of each component.
|
||||
|
||||
**Want to see more examples?** Browse our [Tools documentation](/tools) to see what integrations are available.
|
||||
|
||||
**Ready to deploy?** Learn about [Execution and Deployment](/execution) to make your workflows live.
|
||||
|
||||
@@ -1,84 +1,94 @@
|
||||
---
|
||||
title: Introduction
|
||||
description: The UI for agents
|
||||
description: Build AI workflows visually without code
|
||||
---
|
||||
|
||||
import { Card, Cards } from 'fumadocs-ui/components/card'
|
||||
import { File, Files, Folder } from 'fumadocs-ui/components/files'
|
||||
import { Features } from '@/components/ui/features'
|
||||
|
||||
Sim Studio is a powerful platform for building, testing, and optimizing agentic workflows. It provides developers with intuitive tools to design sophisticated agent-based applications through a visual interface. Whether you're prototyping a simple AI assistant or building complex multi-agent systems, Sim Studio offers the flexibility and performance needed for modern AI applications.
|
||||
Sim Studio is a visual workflow editor that enables you to build AI-powered applications by connecting blocks on a canvas. Drag and drop components to create chatbots, automation workflows, and data processing pipelines without writing code.
|
||||
|
||||
## Why Sim Studio?
|
||||
## What Makes Sim Studio Powerful
|
||||
|
||||
Building agentic applications requires extensive coding and integration work. Developers spend more time on infrastructure and plumbing than focusing on core AI logic. Sim Studio changes this with a <span className="text-highlight">comprehensive visual workflow editor</span> that handles complexity while keeping you in control of what matters.
|
||||
**Multi-Model AI Support** - Connect to OpenAI, Anthropic, Google, Groq, Cerebras, and local models through Ollama. Switch providers without rebuilding workflows.
|
||||
|
||||
## How Sim Studio Differs
|
||||
**60+ Pre-Built Tools** - Gmail, Slack, Notion, Google Sheets, Airtable, Supabase, Pinecone, and more. Extensible architecture allows custom tool integration.
|
||||
|
||||
Sim Studio takes a fundamentally different approach from existing agent development solutions:
|
||||
**Flexible Execution** - Run workflows via chat interface, REST API, webhooks, scheduled jobs, or trigger from external systems.
|
||||
|
||||
### Focus on What Matters
|
||||
**Production Deployment** - Deploy as APIs, integrate with existing systems using our SDK, or embed as plugins. Built-in monitoring, logging, and error handling.
|
||||
|
||||
Developers waste countless hours customizing multi-agent frameworks, writing boilerplate code, creating integrations, and building tooling from scratch. By the time they've configured their environment, precious resources are consumed before any real agent work begins.
|
||||
**Real-time Collaboration** - Work simultaneously with team members on the same workflow, like Google Docs for AI development.
|
||||
|
||||
Sim Studio eliminates this overhead. We've distilled agent development to its essence, removing boilerplate and infrastructure complexity. With Sim Studio, you can <span className="text-highlight">immediately start designing intelligence</span> rather than wrestling with configuration. Your time is best spent refining agent behaviors, not writing glue code that doesn't improve the end-user experience.
|
||||
Connect multiple AI models, integrate with 60+ services, and deploy production-ready applications through an intuitive visual interface designed specifically for AI development.
|
||||
|
||||
### Provider-Aligned Interfaces
|
||||
## Core Building Blocks
|
||||
|
||||
Existing frameworks abstract away provider-specific features, forcing developers to navigate layers of abstraction to access specific capabilities. The result: lost functionality, reduced flexibility, and additional code to bridge these gaps.
|
||||
**Processing Blocks**
|
||||
|
||||
Sim Studio stays <span className="text-highlight">close to provider definitions</span>, directly exposing the parameters that matter:
|
||||
- **Agent** - Execute AI model inference with any LLM provider
|
||||
- **API** - Connect to REST endpoints and external services
|
||||
- **Function** - Run custom JavaScript for data processing
|
||||
|
||||
<ul className="highlight-markers list-disc space-y-2 pl-6">
|
||||
<li>System prompts and instructions with native formatting</li>
|
||||
<li>Tool definitions and access patterns that match provider implementations</li>
|
||||
<li>Temperature and sampling parameters with their full range of options</li>
|
||||
<li>Structured output formatting that aligns with provider capabilities</li>
|
||||
<li>Model selection and configuration with provider-specific optimizations</li>
|
||||
</ul>
|
||||
**Logic Blocks**
|
||||
|
||||
This approach gives you full control over agent behavior without unnecessary complexity. You leverage each provider's full capabilities without sacrificing the convenience of a unified platform.
|
||||
- **Condition** - Create branching logic based on data evaluation
|
||||
- **Router** - Route execution paths using AI-powered decision making
|
||||
- **Loop** - Iterate over collections sequentially
|
||||
- **Parallel** - Execute multiple operations concurrently
|
||||
|
||||
### Unified Model Interface
|
||||
**Output Blocks**
|
||||
|
||||
Most environments lock you into a specific LLM provider early in development. Changing providers later requires significant refactoring, often affecting your entire application architecture and limiting your ability to leverage advances in model capabilities.
|
||||
- **Response** - Format and return final workflow results
|
||||
- **Evaluator** - Validate outputs against defined criteria
|
||||
|
||||
Our platform provides a <span className="text-highlight">consistent interface across different AI models</span>, letting you switch between OpenAI, Anthropic, Claude, Llama, Gemini and others without rewriting your agent logic. This model-agnostic approach future-proofs your applications and gives you freedom to select the best model for each specific use case—optimize for cost with one agent and performance with another, all within the same workflow.
|
||||
## Built-in Integrations
|
||||
|
||||
### AI-Native Design
|
||||
**AI Models**: OpenAI, Anthropic, Google, Groq, Cerebras, Ollama
|
||||
|
||||
Traditional development environments were designed for conventional software and later adapted for AI. These adaptations often feel like afterthoughts, with AI capabilities awkwardly grafted onto existing paradigms.
|
||||
**Communication**: Gmail, Slack, Telegram, WhatsApp, Microsoft Teams
|
||||
|
||||
Sim Studio is <span className="text-highlight">built from the ground up as an AI-native application</span>. Every aspect—from the visual workflow editor to testing environments—is designed specifically for agent development. Common AI development patterns are first-class concepts in our platform, not workarounds. Testing prompts, adjusting parameters, or implementing complex tool calling patterns feel natural because they're core to our design philosophy.
|
||||
**Data Sources**: Notion, Google Sheets, Airtable, Supabase, Pinecone
|
||||
|
||||
### Local Development Support
|
||||
**Web Services**: Firecrawl, Google Search, Exa AI, Perplexity
|
||||
|
||||
Many AI platforms force you to develop against cloud APIs, creating dependencies on internet connectivity, increasing costs, and introducing privacy concerns with sensitive data.
|
||||
**Development**: GitHub, Jira, Linear, browser automation
|
||||
|
||||
Sim Studio supports <span className="text-highlight">full local development</span> using Ollama integration. Develop with privacy-preserving local models, then seamlessly deploy with cloud providers in production. This hybrid approach gives you the best of both worlds: privacy, cost-efficiency, and reliability during development, with scalability and performance in production.
|
||||
## Use Cases
|
||||
|
||||
### Comprehensive Observability
|
||||
**AI Assistants** - Build chatbots with web search, calendar access, and email capabilities
|
||||
|
||||
Existing solutions provide limited visibility into agent performance, making it difficult to identify bottlenecks, understand costs, or diagnose failures. Developers build custom instrumentation or operate in the dark.
|
||||
**Content Generation** - Create blog posts, social media content, and marketing materials
|
||||
|
||||
Sim Studio provides <span className="text-highlight">full visibility into agent performance</span> with integrated observability:
|
||||
**Data Processing** - Extract insights from documents, analyze datasets, and generate reports
|
||||
|
||||
<ul className="highlight-markers list-disc space-y-2 pl-6">
|
||||
<li>Detailed execution logs capturing every interaction between agents and models</li>
|
||||
<li>Latency tracing with span visualization to identify performance bottlenecks</li>
|
||||
<li>Cost tracking and optimization to prevent budget overruns</li>
|
||||
<li>Error analysis and debugging tools for complex workflows</li>
|
||||
<li>Performance comparisons across different model configurations</li>
|
||||
</ul>
|
||||
**Process Automation** - Automate business workflows with event-driven triggers
|
||||
|
||||
This comprehensive observability means less time investigating issues and more time resolving them—faster development cycles and more reliable agent workflows.
|
||||
**API Orchestration** - Combine multiple services into unified endpoints
|
||||
|
||||
## Features
|
||||
## Key Features
|
||||
|
||||
Sim Studio provides a wide range of features designed to accelerate your development process:
|
||||
**Multi-Provider AI Support** - Switch between OpenAI, Anthropic, Google, and local models without rebuilding workflows
|
||||
|
||||
<Features />
|
||||
**Real-time Collaboration** - Work simultaneously with team members on the same workflow
|
||||
|
||||
##
|
||||
**Production-Ready** - Built-in error handling, logging, and monitoring for production deployments
|
||||
|
||||
Ready to get started? Check out our [Getting Started](/getting-started) guide or explore our [Blocks](/blocks) and [Tools](/tools) in more detail.
|
||||
**Local Development** - Test with Ollama locally, then deploy with cloud providers
|
||||
|
||||
## Getting Started
|
||||
|
||||
Ready to build your first workflow? Our [Getting Started guide](/getting-started) will walk you through creating a customer support assistant in under 10 minutes.
|
||||
|
||||
<Cards>
|
||||
<Card title="Getting Started" href="/getting-started">
|
||||
Build your first workflow
|
||||
</Card>
|
||||
<Card title="Blocks" href="/blocks">
|
||||
Learn about workflow components
|
||||
</Card>
|
||||
<Card title="Tools" href="/tools">
|
||||
Explore integrations
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
@@ -102,6 +102,46 @@ Draft emails using Gmail
|
||||
| `threadId` | string |
|
||||
| `labelIds` | string |
|
||||
|
||||
### `gmail_read`
|
||||
|
||||
Read emails from Gmail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Gmail API |
|
||||
| `messageId` | string | No | ID of the message to read |
|
||||
| `folder` | string | No | Folder/label to read emails from |
|
||||
| `unreadOnly` | boolean | No | Only retrieve unread messages |
|
||||
| `maxResults` | number | No | Maximum number of messages to retrieve \(default: 1, max: 10\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `content` | string |
|
||||
| `metadata` | string |
|
||||
|
||||
### `gmail_search`
|
||||
|
||||
Search emails in Gmail
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Gmail API |
|
||||
| `query` | string | Yes | Search query for emails |
|
||||
| `maxResults` | number | No | Maximum number of results to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `content` | string |
|
||||
| `metadata` | string |
|
||||
|
||||
|
||||
|
||||
## Block Configuration
|
||||
|
||||
@@ -43,6 +43,23 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
</svg>`}
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Amazon S3](https://aws.amazon.com/s3/) is a highly scalable, secure, and durable cloud storage service provided by Amazon Web Services. It's designed to store and retrieve any amount of data from anywhere on the web, making it one of the most widely used cloud storage solutions for businesses of all sizes.
|
||||
|
||||
With Amazon S3, you can:
|
||||
|
||||
- **Store unlimited data**: Upload files of any size and type with virtually unlimited storage capacity
|
||||
- **Access from anywhere**: Retrieve your files from anywhere in the world with low-latency access
|
||||
- **Ensure data durability**: Benefit from 99.999999999% (11 9's) durability with automatic data replication
|
||||
- **Control access**: Manage permissions and access controls with fine-grained security policies
|
||||
- **Scale automatically**: Handle varying workloads without manual intervention or capacity planning
|
||||
- **Integrate seamlessly**: Connect with other AWS services and third-party applications easily
|
||||
- **Optimize costs**: Choose from multiple storage classes to optimize costs based on access patterns
|
||||
|
||||
In Sim Studio, the S3 integration enables your agents to retrieve and access files stored in your Amazon S3 buckets using secure presigned URLs. This allows for powerful automation scenarios such as processing documents, analyzing stored data, retrieving configuration files, and accessing media content as part of your workflows. Your agents can securely fetch files from S3 without exposing your AWS credentials, making it easy to incorporate cloud-stored assets into your automation processes. This integration bridges the gap between your cloud storage and AI workflows, enabling seamless access to your stored data while maintaining security best practices through AWS's robust authentication mechanisms.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Retrieve and view files from Amazon S3 buckets using presigned URLs.
|
||||
|
||||
@@ -11,30 +11,17 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
icon={true}
|
||||
iconSvg={`<svg className="block-icon"
|
||||
|
||||
|
||||
|
||||
viewBox='-5 0 41 33'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 24 24'
|
||||
|
||||
|
||||
fill='none'
|
||||
>
|
||||
<circle cx='16' cy='16' r='14' fill='url(#paint0_linear_87_7225)' />
|
||||
<circle cx='12' cy='12' r='10' fill='#0088CC' />
|
||||
<path
|
||||
d='M22.9866 10.2088C23.1112 9.40332 22.3454 8.76755 21.6292 9.082L7.36482 15.3448C6.85123 15.5703 6.8888 16.3483 7.42147 16.5179L10.3631 17.4547C10.9246 17.6335 11.5325 17.541 12.0228 17.2023L18.655 12.6203C18.855 12.4821 19.073 12.7665 18.9021 12.9426L14.1281 17.8646C13.665 18.3421 13.7569 19.1512 14.314 19.5005L19.659 22.8523C20.2585 23.2282 21.0297 22.8506 21.1418 22.1261L22.9866 10.2088Z'
|
||||
d='M16.7 8.4c.1-.6-.4-1.1-1-.8l-9.8 4.3c-.4.2-.4.8.1.9l2.1.7c.4.1.8.1 1.1-.2l4.5-3.1c.1-.1.3.1.2.2l-3.2 3.5c-.3.3-.2.8.2 1l3.6 2.3c.4.2.9-.1 1-.5l1.2-7.8Z'
|
||||
fill='white'
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id='paint0_linear_87_7225'
|
||||
x1='16'
|
||||
y1='2'
|
||||
x2='16'
|
||||
y2='30'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop stopColor='#37BBFE' />
|
||||
<stop offset='1' stopColor='#007DBB' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>`}
|
||||
/>
|
||||
|
||||
|
||||
@@ -24,6 +24,22 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
</svg>`}
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Wealthbox](https://www.wealthbox.com/) is a comprehensive CRM platform designed specifically for financial advisors and wealth management professionals. It provides a centralized system for managing client relationships, tracking interactions, and organizing business workflows in the financial services industry.
|
||||
|
||||
With Wealthbox, you can:
|
||||
|
||||
- **Manage client relationships**: Store detailed contact information, background data, and relationship histories for all your clients
|
||||
- **Track interactions**: Create and maintain notes about meetings, calls, and other client touchpoints
|
||||
- **Organize tasks**: Schedule and manage follow-up activities, deadlines, and important action items
|
||||
- **Document workflows**: Keep comprehensive records of client communications and business processes
|
||||
- **Access client data**: Retrieve information quickly with organized contact management and search capabilities
|
||||
- **Automate follow-ups**: Set reminders and schedule tasks to ensure consistent client engagement
|
||||
|
||||
In Sim Studio, the Wealthbox integration enables your agents to seamlessly interact with your CRM data through OAuth authentication. This allows for powerful automation scenarios such as automatically creating client notes from meeting transcripts, updating contact information, scheduling follow-up tasks, and retrieving client details for personalized communications. Your agents can read existing notes, contacts, and tasks to understand client history, while also creating new entries to maintain up-to-date records. This integration bridges the gap between your AI workflows and your client relationship management, enabling automated data entry, intelligent client insights, and streamlined administrative processes that free up time for more valuable client-facing activities.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Wealthbox functionality to manage notes, contacts, and tasks. Read content from existing notes, contacts, and tasks and write to them using OAuth authentication. Supports text content manipulation for note creation and editing.
|
||||
@@ -41,7 +57,7 @@ Read content from a Wealthbox note
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `noteId` | string | No | The ID of the note to read \(optional\) |
|
||||
| `noteId` | string | No | The ID of the note to read |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -68,9 +84,7 @@ Create or update a Wealthbox note
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `note` | string |
|
||||
| `metadata` | string |
|
||||
| `itemType` | string |
|
||||
| `data` | json |
|
||||
|
||||
### `wealthbox_read_contact`
|
||||
|
||||
@@ -81,7 +95,7 @@ Read content from a Wealthbox contact
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `contactId` | string | Yes | The ID of the contact to read |
|
||||
| `contactId` | string | No | The ID of the contact to read |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -123,7 +137,7 @@ Read content from a Wealthbox task
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `taskId` | string | No | The ID of the task to read \(optional\) |
|
||||
| `taskId` | string | No | The ID of the task to read |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -144,19 +158,15 @@ Create or update a Wealthbox task
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `title` | string | Yes | The name/title of the task |
|
||||
| `dueDate` | string | Yes | The due date and time of the task |
|
||||
| `complete` | boolean | No | Whether the task is complete |
|
||||
| `category` | number | No | The category ID the task belongs to |
|
||||
| `dueDate` | string | Yes | The due date and time of the task \(format: |
|
||||
| `contactId` | string | No | ID of contact to link to this task |
|
||||
| `description` | string | No | Description or notes about the task |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `task` | string |
|
||||
| `metadata` | string |
|
||||
| `taskId` | string |
|
||||
| `itemType` | string |
|
||||
| `data` | json |
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
apps/docs/public/connections-resp-format.mp4
Normal file
BIN
apps/docs/public/granular-tool-control.mp4
Normal file
BIN
apps/docs/public/models.mp4
Normal file
BIN
apps/docs/public/router-model-dropdown.mp4
Normal file
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 51 KiB |
BIN
apps/docs/public/static/dark/knowledge-dark.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
apps/docs/public/static/dark/loop-dark.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
apps/docs/public/static/dark/parallel-dark.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 70 KiB |
BIN
apps/docs/public/static/examples/started/started-1.png
Normal file
|
After Width: | Height: | Size: 391 KiB |
BIN
apps/docs/public/static/examples/started/started-2.mp4
Normal file
BIN
apps/docs/public/static/examples/started/started-3.mp4
Normal file
BIN
apps/docs/public/static/examples/started/started-4.mp4
Normal file
BIN
apps/docs/public/static/examples/started/started-5.mp4
Normal file
BIN
apps/docs/public/static/examples/started/started-6.mp4
Normal file
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 47 KiB |
BIN
apps/docs/public/static/light/knowledge-light.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
apps/docs/public/static/light/loop-light.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
apps/docs/public/static/light/parallel-light.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 118 KiB |
BIN
apps/docs/public/static/light/router-example.png
Normal file
|
After Width: | Height: | Size: 467 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 64 KiB |
BIN
apps/docs/public/tool-reordering.mp4
Normal file
BIN
apps/docs/public/tools.mp4
Normal file
@@ -5,7 +5,6 @@ import { GithubIcon, GoogleIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
|
||||
interface SocialLoginButtonsProps {
|
||||
githubAvailable: boolean
|
||||
@@ -22,7 +21,6 @@ export function SocialLoginButtons({
|
||||
}: SocialLoginButtonsProps) {
|
||||
const [isGithubLoading, setIsGithubLoading] = useState(false)
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false)
|
||||
const { addNotification } = useNotificationStore()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
// Set mounted state to true on client-side
|
||||
@@ -57,8 +55,6 @@ export function SocialLoginButtons({
|
||||
} else if (err.message?.includes('rate limit')) {
|
||||
errorMessage = 'Too many attempts. Please try again later.'
|
||||
}
|
||||
|
||||
addNotification('error', errorMessage, null)
|
||||
} finally {
|
||||
setIsGithubLoading(false)
|
||||
}
|
||||
@@ -89,8 +85,6 @@ export function SocialLoginButtons({
|
||||
} else if (err.message?.includes('rate limit')) {
|
||||
errorMessage = 'Too many attempts. Please try again later.'
|
||||
}
|
||||
|
||||
addNotification('error', errorMessage, null)
|
||||
} finally {
|
||||
setIsGoogleLoading(false)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { GridPattern } from '../(landing)/components/grid-pattern'
|
||||
import { NotificationList } from '../workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications'
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -31,11 +30,6 @@ export default function AuthLayout({ children }: { children: React.ReactNode })
|
||||
<div className='relative z-10 flex flex-1 items-center justify-center px-4 pb-6'>
|
||||
<div className='w-full max-w-md'>{children}</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className='fixed right-4 bottom-4 z-50'>
|
||||
<NotificationList />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import { env, isTruthy } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
|
||||
const logger = createLogger('useVerification')
|
||||
|
||||
@@ -35,7 +34,6 @@ export function useVerification({
|
||||
}: UseVerificationParams): UseVerificationReturn {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { addNotification } = useNotificationStore()
|
||||
const [otp, setOtp] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -46,13 +44,6 @@ export function useVerification({
|
||||
const [redirectUrl, setRedirectUrl] = useState<string | null>(null)
|
||||
const [isInviteFlow, setIsInviteFlow] = useState(false)
|
||||
|
||||
// Debug notification store
|
||||
useEffect(() => {
|
||||
logger.info('Notification store state:', {
|
||||
addNotification: !!addNotification,
|
||||
})
|
||||
}, [addNotification])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Get stored email
|
||||
|
||||
@@ -30,6 +30,8 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
orderBy: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn().mockReturnThis(),
|
||||
offset: vi.fn().mockReturnThis(),
|
||||
insert: vi.fn().mockReturnThis(),
|
||||
values: vi.fn().mockReturnThis(),
|
||||
update: vi.fn().mockReturnThis(),
|
||||
@@ -99,7 +101,12 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
it('should retrieve documents successfully for authenticated user', async () => {
|
||||
mockAuth$.mockAuthenticatedUser()
|
||||
mockCheckKnowledgeBaseAccess.mockResolvedValue({ hasAccess: true })
|
||||
mockDbChain.orderBy.mockResolvedValue([mockDocument])
|
||||
|
||||
// Mock the count query (first query)
|
||||
mockDbChain.where.mockResolvedValueOnce([{ count: 1 }])
|
||||
|
||||
// Mock the documents query (second query)
|
||||
mockDbChain.offset.mockResolvedValue([mockDocument])
|
||||
|
||||
const req = createMockRequest('GET')
|
||||
const { GET } = await import('./route')
|
||||
@@ -108,8 +115,8 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
expect(data.data).toHaveLength(1)
|
||||
expect(data.data[0].id).toBe('doc-123')
|
||||
expect(data.data.documents).toHaveLength(1)
|
||||
expect(data.data.documents[0].id).toBe('doc-123')
|
||||
expect(mockDbChain.select).toHaveBeenCalled()
|
||||
expect(mockCheckKnowledgeBaseAccess).toHaveBeenCalledWith('kb-123', 'user-123')
|
||||
})
|
||||
@@ -117,7 +124,12 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
it('should filter disabled documents by default', async () => {
|
||||
mockAuth$.mockAuthenticatedUser()
|
||||
mockCheckKnowledgeBaseAccess.mockResolvedValue({ hasAccess: true })
|
||||
mockDbChain.orderBy.mockResolvedValue([mockDocument])
|
||||
|
||||
// Mock the count query (first query)
|
||||
mockDbChain.where.mockResolvedValueOnce([{ count: 1 }])
|
||||
|
||||
// Mock the documents query (second query)
|
||||
mockDbChain.offset.mockResolvedValue([mockDocument])
|
||||
|
||||
const req = createMockRequest('GET')
|
||||
const { GET } = await import('./route')
|
||||
@@ -130,7 +142,12 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
it('should include disabled documents when requested', async () => {
|
||||
mockAuth$.mockAuthenticatedUser()
|
||||
mockCheckKnowledgeBaseAccess.mockResolvedValue({ hasAccess: true })
|
||||
mockDbChain.orderBy.mockResolvedValue([mockDocument])
|
||||
|
||||
// Mock the count query (first query)
|
||||
mockDbChain.where.mockResolvedValueOnce([{ count: 1 }])
|
||||
|
||||
// Mock the documents query (second query)
|
||||
mockDbChain.offset.mockResolvedValue([mockDocument])
|
||||
|
||||
const url = 'http://localhost:3000/api/knowledge/kb-123/documents?includeDisabled=true'
|
||||
const req = new Request(url, { method: 'GET' }) as any
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import crypto from 'node:crypto'
|
||||
import { and, desc, eq, inArray, isNull } from 'drizzle-orm'
|
||||
import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getUserId } from '@/app/api/auth/oauth/utils'
|
||||
import { db } from '@/db'
|
||||
import { document } from '@/db/schema'
|
||||
import { checkKnowledgeBaseAccess, processDocumentAsync } from '../../utils'
|
||||
@@ -209,6 +210,9 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
|
||||
const url = new URL(req.url)
|
||||
const includeDisabled = url.searchParams.get('includeDisabled') === 'true'
|
||||
const search = url.searchParams.get('search')
|
||||
const limit = Number.parseInt(url.searchParams.get('limit') || '50')
|
||||
const offset = Number.parseInt(url.searchParams.get('offset') || '0')
|
||||
|
||||
// Build where conditions
|
||||
const whereConditions = [
|
||||
@@ -221,6 +225,23 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
whereConditions.push(eq(document.enabled, true))
|
||||
}
|
||||
|
||||
// Add search condition if provided
|
||||
if (search) {
|
||||
whereConditions.push(
|
||||
// Search in filename
|
||||
sql`LOWER(${document.filename}) LIKE LOWER(${`%${search}%`})`
|
||||
)
|
||||
}
|
||||
|
||||
// Get total count for pagination
|
||||
const totalResult = await db
|
||||
.select({ count: sql<number>`COUNT(*)` })
|
||||
.from(document)
|
||||
.where(and(...whereConditions))
|
||||
|
||||
const total = totalResult[0]?.count || 0
|
||||
const hasMore = offset + limit < total
|
||||
|
||||
const documents = await db
|
||||
.select({
|
||||
id: document.id,
|
||||
@@ -249,14 +270,24 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
.from(document)
|
||||
.where(and(...whereConditions))
|
||||
.orderBy(desc(document.uploadedAt))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Retrieved ${documents.length} documents for knowledge base ${knowledgeBaseId}`
|
||||
`[${requestId}] Retrieved ${documents.length} documents (${offset}-${offset + documents.length} of ${total}) for knowledge base ${knowledgeBaseId}`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: documents,
|
||||
data: {
|
||||
documents,
|
||||
pagination: {
|
||||
total,
|
||||
limit,
|
||||
offset,
|
||||
hasMore,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching documents`, error)
|
||||
@@ -269,13 +300,29 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
const { id: knowledgeBaseId } = await params
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized document creation attempt`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
const body = await req.json()
|
||||
const { workflowId } = body
|
||||
|
||||
logger.info(`[${requestId}] Knowledge base document creation request`, {
|
||||
knowledgeBaseId,
|
||||
workflowId,
|
||||
hasWorkflowId: !!workflowId,
|
||||
bodyKeys: Object.keys(body),
|
||||
})
|
||||
|
||||
const userId = await getUserId(requestId, workflowId)
|
||||
|
||||
if (!userId) {
|
||||
const errorMessage = workflowId ? 'Workflow not found' : 'Unauthorized'
|
||||
const statusCode = workflowId ? 404 : 401
|
||||
logger.warn(`[${requestId}] Authentication failed: ${errorMessage}`, {
|
||||
workflowId,
|
||||
hasWorkflowId: !!workflowId,
|
||||
})
|
||||
return NextResponse.json({ error: errorMessage }, { status: statusCode })
|
||||
}
|
||||
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id)
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, userId)
|
||||
|
||||
if (!accessCheck.hasAccess) {
|
||||
if ('notFound' in accessCheck && accessCheck.notFound) {
|
||||
@@ -283,13 +330,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
return NextResponse.json({ error: 'Knowledge base not found' }, { status: 404 })
|
||||
}
|
||||
logger.warn(
|
||||
`[${requestId}] User ${session.user.id} attempted to create document in unauthorized knowledge base ${knowledgeBaseId}`
|
||||
`[${requestId}] User ${userId} attempted to create document in unauthorized knowledge base ${knowledgeBaseId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
|
||||
// Check if this is a bulk operation
|
||||
if (body.bulk === true) {
|
||||
// Handle bulk processing (replaces process-documents endpoint)
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('Knowledge Utils', () => {
|
||||
{}
|
||||
)
|
||||
|
||||
expect(dbOps.order).toEqual(['insert', 'updateDoc', 'updateKb'])
|
||||
expect(dbOps.order).toEqual(['insert', 'updateDoc'])
|
||||
|
||||
expect(dbOps.updatePayloads[0]).toMatchObject({
|
||||
processingStatus: 'completed',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import crypto from 'crypto'
|
||||
import { and, eq, isNull, sql } from 'drizzle-orm'
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
import { processDocument } from '@/lib/documents/document-processor'
|
||||
import { retryWithExponentialBackoff } from '@/lib/documents/utils'
|
||||
import { env } from '@/lib/env'
|
||||
@@ -522,14 +522,6 @@ export async function processDocumentAsync(
|
||||
processingError: null,
|
||||
})
|
||||
.where(eq(document.id, documentId))
|
||||
|
||||
await tx
|
||||
.update(knowledgeBase)
|
||||
.set({
|
||||
tokenCount: sql`${knowledgeBase.tokenCount} + ${processed.metadata.tokenCount}`,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(knowledgeBase.id, knowledgeBaseId))
|
||||
})
|
||||
})(),
|
||||
TIMEOUTS.OVERALL_PROCESSING,
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import { db } from '@/db'
|
||||
import { marketplace } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('MarketplaceInfoAPI')
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
// Validate access to the workflow
|
||||
const validation = await validateWorkflowAccess(request, id, false)
|
||||
if (validation.error) {
|
||||
logger.warn(`[${requestId}] Workflow access validation failed: ${validation.error.message}`)
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
// Fetch marketplace data for the workflow
|
||||
const marketplaceEntry = await db
|
||||
.select()
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.workflowId, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found for workflow: ${id}`)
|
||||
return createErrorResponse('Workflow is not published to marketplace', 404)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Retrieved marketplace info for workflow: ${id}`)
|
||||
|
||||
return createSuccessResponse({
|
||||
id: marketplaceEntry.id,
|
||||
name: marketplaceEntry.name,
|
||||
description: marketplaceEntry.description,
|
||||
category: marketplaceEntry.category,
|
||||
authorName: marketplaceEntry.authorName,
|
||||
views: marketplaceEntry.views,
|
||||
createdAt: marketplaceEntry.createdAt,
|
||||
updatedAt: marketplaceEntry.updatedAt,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[${requestId}] Error getting marketplace info for workflow: ${(await params).id}`,
|
||||
error
|
||||
)
|
||||
return createErrorResponse('Failed to get marketplace information', 500)
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import { db } from '@/db'
|
||||
import { marketplace, workflow } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('MarketplaceUnpublishAPI')
|
||||
|
||||
/**
|
||||
* API endpoint to unpublish a workflow from the marketplace by its marketplace ID
|
||||
*
|
||||
* Security:
|
||||
* - Requires authentication
|
||||
* - Validates that the current user is the author of the marketplace entry
|
||||
* - Only allows the owner to unpublish
|
||||
*/
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
// Get the session first for authorization
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized unpublish attempt for marketplace ID: ${id}`)
|
||||
return createErrorResponse('Unauthorized', 401)
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Get the marketplace entry using the marketplace ID
|
||||
const marketplaceEntry = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
workflowId: marketplace.workflowId,
|
||||
authorId: marketplace.authorId,
|
||||
name: marketplace.name,
|
||||
})
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.id, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found with ID: ${id}`)
|
||||
return createErrorResponse('Marketplace entry not found', 404)
|
||||
}
|
||||
|
||||
// Check if the user is the author of the marketplace entry
|
||||
if (marketplaceEntry.authorId !== userId) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${userId} tried to unpublish marketplace entry they don't own: ${id}, author: ${marketplaceEntry.authorId}`
|
||||
)
|
||||
return createErrorResponse('You do not have permission to unpublish this workflow', 403)
|
||||
}
|
||||
|
||||
const workflowId = marketplaceEntry.workflowId
|
||||
|
||||
// Verify the workflow exists and belongs to the user
|
||||
const workflowEntry = await db
|
||||
.select({
|
||||
id: workflow.id,
|
||||
userId: workflow.userId,
|
||||
})
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!workflowEntry) {
|
||||
logger.warn(`[${requestId}] Associated workflow not found: ${workflowId}`)
|
||||
// We'll still delete the marketplace entry even if the workflow is missing
|
||||
} else if (workflowEntry.userId !== userId) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workflow ${workflowId} belongs to user ${workflowEntry.userId}, not current user ${userId}`
|
||||
)
|
||||
return createErrorResponse('You do not have permission to unpublish this workflow', 403)
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete the marketplace entry - this is the primary action
|
||||
await db.delete(marketplace).where(eq(marketplace.id, id))
|
||||
|
||||
// Update the workflow to mark it as unpublished if it exists
|
||||
if (workflowEntry) {
|
||||
await db.update(workflow).set({ isPublished: false }).where(eq(workflow.id, workflowId))
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Workflow "${marketplaceEntry.name}" unpublished from marketplace: ID=${id}, workflowId=${workflowId}`
|
||||
)
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
message: 'Workflow successfully unpublished from marketplace',
|
||||
})
|
||||
} catch (dbError) {
|
||||
logger.error(`[${requestId}] Database error unpublishing marketplace entry:`, dbError)
|
||||
return createErrorResponse('Failed to unpublish workflow due to a database error', 500)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error unpublishing marketplace entry: ${(await params).id}`, error)
|
||||
return createErrorResponse('Failed to unpublish workflow', 500)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import { db } from '@/db'
|
||||
import { marketplace } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('MarketplaceViewAPI')
|
||||
|
||||
/**
|
||||
* POST handler for incrementing the view count when a workflow card is clicked
|
||||
* This endpoint is called from the WorkflowCard component's onClick handler
|
||||
*
|
||||
* The ID parameter is the marketplace entry ID, not the workflow ID
|
||||
*/
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
// Find the marketplace entry for this marketplace ID
|
||||
const marketplaceEntry = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
})
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.id, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found with ID: ${id}`)
|
||||
return createErrorResponse('Marketplace entry not found', 404)
|
||||
}
|
||||
|
||||
// Increment the view count for this workflow
|
||||
await db
|
||||
.update(marketplace)
|
||||
.set({
|
||||
views: sql`${marketplace.views} + 1`,
|
||||
})
|
||||
.where(eq(marketplace.id, id))
|
||||
|
||||
logger.info(`[${requestId}] Incremented view count for marketplace entry: ${id}`)
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[${requestId}] Error incrementing view count for marketplace entry: ${(await params).id}`,
|
||||
error
|
||||
)
|
||||
return createErrorResponse('Failed to track view', 500)
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { marketplace, user, workflow } from '@/db/schema'
|
||||
|
||||
// Create a logger for this module
|
||||
const logger = createLogger('MarketplacePublishAPI')
|
||||
|
||||
// No cache
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const revalidate = 0
|
||||
|
||||
// Schema for request body
|
||||
const PublishRequestSchema = z.object({
|
||||
workflowId: z.string().uuid(),
|
||||
name: z.string().min(3).max(50).optional(),
|
||||
description: z.string().min(10).max(500).optional(),
|
||||
category: z.string().min(1).optional(),
|
||||
authorName: z.string().min(2).max(50).optional(),
|
||||
workflowState: z.record(z.any()).optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
// Get the session directly in the API route
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized marketplace publish attempt`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
try {
|
||||
// Parse request body
|
||||
const body = await request.json()
|
||||
const { workflowId, name, description, category, authorName, workflowState } =
|
||||
PublishRequestSchema.parse(body)
|
||||
|
||||
// Check if the workflow belongs to the user
|
||||
const userWorkflow = await db
|
||||
.select({ id: workflow.id, name: workflow.name, description: workflow.description })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!userWorkflow.length || userWorkflow[0].id !== workflowId) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${userId} attempted to publish workflow they don't own: ${workflowId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Get the user's name for attribution
|
||||
const userData = await db
|
||||
.select({ name: user.name })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.limit(1)
|
||||
|
||||
if (!userData.length) {
|
||||
logger.error(`[${requestId}] User data not found for ID: ${userId}`)
|
||||
return NextResponse.json({ error: 'User data not found' }, { status: 500 })
|
||||
}
|
||||
|
||||
// Verify we have the workflow state
|
||||
if (!workflowState) {
|
||||
logger.error(`[${requestId}] No workflow state provided for ID: ${workflowId}`)
|
||||
return NextResponse.json({ error: 'Workflow state is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if this workflow is already published
|
||||
const existingPublication = await db
|
||||
.select({ id: marketplace.id })
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.workflowId, workflowId))
|
||||
.limit(1)
|
||||
|
||||
let result
|
||||
const marketplaceId = existingPublication.length ? existingPublication[0].id : uuidv4()
|
||||
|
||||
// Prepare the marketplace entry
|
||||
const marketplaceEntry = {
|
||||
id: marketplaceId,
|
||||
workflowId,
|
||||
state: workflowState,
|
||||
name: name || userWorkflow[0].name,
|
||||
description: description || userWorkflow[0].description || '',
|
||||
authorId: userId,
|
||||
authorName: authorName || userData[0].name,
|
||||
category: category || null,
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
if (existingPublication.length) {
|
||||
// Update existing entry
|
||||
result = await db
|
||||
.update(marketplace)
|
||||
.set(marketplaceEntry)
|
||||
.where(eq(marketplace.id, marketplaceId))
|
||||
.returning()
|
||||
} else {
|
||||
// Create new entry with createdAt
|
||||
result = await db
|
||||
.insert(marketplace)
|
||||
.values({
|
||||
...marketplaceEntry,
|
||||
createdAt: new Date(),
|
||||
views: 0,
|
||||
})
|
||||
.returning()
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully published workflow to marketplace`, {
|
||||
workflowId,
|
||||
marketplaceId,
|
||||
userId,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Workflow published successfully',
|
||||
data: {
|
||||
id: result[0].id,
|
||||
workflowId: result[0].workflowId,
|
||||
name: result[0].name,
|
||||
},
|
||||
})
|
||||
} catch (validationError) {
|
||||
if (validationError instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid marketplace publish request parameters`, {
|
||||
errors: validationError.errors,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Invalid request parameters',
|
||||
details: validationError.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
throw validationError
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Marketplace publish error`, error)
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
import { desc, eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import { CATEGORIES } from '@/app/workspace/[workspaceId]/marketplace/constants/categories'
|
||||
import { db } from '@/db'
|
||||
import { marketplace } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('MarketplaceWorkflowsAPI')
|
||||
|
||||
// Cache for 1 minute but can be revalidated on-demand
|
||||
export const revalidate = 60
|
||||
|
||||
/**
|
||||
* Consolidated API endpoint for marketplace workflows
|
||||
*
|
||||
* Supports:
|
||||
* - Getting featured/popular/recent workflows
|
||||
* - Getting workflows by category
|
||||
* - Getting workflow state
|
||||
* - Getting workflow details
|
||||
* - Incrementing view counts
|
||||
*
|
||||
* Query parameters:
|
||||
* - section: 'popular', 'recent', 'byCategory', or specific category name
|
||||
* - limit: Maximum number of items to return per section (default: 6)
|
||||
* - includeState: Whether to include workflow state in the response (default: false)
|
||||
* - workflowId: Specific workflow ID to fetch details for
|
||||
* - marketplaceId: Specific marketplace entry ID to fetch details for
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
// Parse query parameters
|
||||
const url = new URL(request.url)
|
||||
const sectionParam = url.searchParams.get('section')
|
||||
const categoryParam = url.searchParams.get('category')
|
||||
const limitParam = url.searchParams.get('limit') || '6'
|
||||
const limit = Number.parseInt(limitParam, 10)
|
||||
const includeState = url.searchParams.get('includeState') === 'true'
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
const marketplaceId = url.searchParams.get('marketplaceId')
|
||||
|
||||
// Handle single workflow request first (by workflow ID)
|
||||
if (workflowId) {
|
||||
let marketplaceEntry
|
||||
|
||||
if (includeState) {
|
||||
// Query with state included
|
||||
marketplaceEntry = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
workflowId: marketplace.workflowId,
|
||||
name: marketplace.name,
|
||||
description: marketplace.description,
|
||||
authorId: marketplace.authorId,
|
||||
authorName: marketplace.authorName,
|
||||
state: marketplace.state,
|
||||
views: marketplace.views,
|
||||
category: marketplace.category,
|
||||
createdAt: marketplace.createdAt,
|
||||
updatedAt: marketplace.updatedAt,
|
||||
})
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.workflowId, workflowId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
} else {
|
||||
// Query without state
|
||||
marketplaceEntry = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
workflowId: marketplace.workflowId,
|
||||
name: marketplace.name,
|
||||
description: marketplace.description,
|
||||
authorId: marketplace.authorId,
|
||||
authorName: marketplace.authorName,
|
||||
views: marketplace.views,
|
||||
category: marketplace.category,
|
||||
createdAt: marketplace.createdAt,
|
||||
updatedAt: marketplace.updatedAt,
|
||||
})
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.workflowId, workflowId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
}
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found for workflow: ${workflowId}`)
|
||||
return createErrorResponse('Workflow not found in marketplace', 404)
|
||||
}
|
||||
|
||||
// Transform response if state was requested
|
||||
const responseData =
|
||||
includeState && 'state' in marketplaceEntry
|
||||
? {
|
||||
...marketplaceEntry,
|
||||
workflowState: marketplaceEntry.state,
|
||||
state: undefined,
|
||||
}
|
||||
: marketplaceEntry
|
||||
|
||||
logger.info(`[${requestId}] Retrieved marketplace data for workflow: ${workflowId}`)
|
||||
return createSuccessResponse(responseData)
|
||||
}
|
||||
|
||||
// Handle single marketplace entry request (by marketplace ID)
|
||||
if (marketplaceId) {
|
||||
let marketplaceEntry
|
||||
|
||||
if (includeState) {
|
||||
// Query with state included
|
||||
marketplaceEntry = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
workflowId: marketplace.workflowId,
|
||||
name: marketplace.name,
|
||||
description: marketplace.description,
|
||||
authorId: marketplace.authorId,
|
||||
authorName: marketplace.authorName,
|
||||
state: marketplace.state,
|
||||
views: marketplace.views,
|
||||
category: marketplace.category,
|
||||
createdAt: marketplace.createdAt,
|
||||
updatedAt: marketplace.updatedAt,
|
||||
})
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.id, marketplaceId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
} else {
|
||||
// Query without state
|
||||
marketplaceEntry = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
workflowId: marketplace.workflowId,
|
||||
name: marketplace.name,
|
||||
description: marketplace.description,
|
||||
authorId: marketplace.authorId,
|
||||
authorName: marketplace.authorName,
|
||||
views: marketplace.views,
|
||||
category: marketplace.category,
|
||||
createdAt: marketplace.createdAt,
|
||||
updatedAt: marketplace.updatedAt,
|
||||
})
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.id, marketplaceId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
}
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found with ID: ${marketplaceId}`)
|
||||
return createErrorResponse('Marketplace entry not found', 404)
|
||||
}
|
||||
|
||||
// Transform response if state was requested
|
||||
const responseData =
|
||||
includeState && 'state' in marketplaceEntry
|
||||
? {
|
||||
...marketplaceEntry,
|
||||
workflowState: marketplaceEntry.state,
|
||||
state: undefined,
|
||||
}
|
||||
: marketplaceEntry
|
||||
|
||||
logger.info(`[${requestId}] Retrieved marketplace entry: ${marketplaceId}`)
|
||||
return createSuccessResponse(responseData)
|
||||
}
|
||||
|
||||
// Handle featured/collection requests
|
||||
const result: {
|
||||
popular: any[]
|
||||
recent: any[]
|
||||
byCategory: Record<string, any[]>
|
||||
} = {
|
||||
popular: [],
|
||||
recent: [],
|
||||
byCategory: {},
|
||||
}
|
||||
|
||||
// Define common fields to select
|
||||
const baseFields = {
|
||||
id: marketplace.id,
|
||||
workflowId: marketplace.workflowId,
|
||||
name: marketplace.name,
|
||||
description: marketplace.description,
|
||||
authorName: marketplace.authorName,
|
||||
views: marketplace.views,
|
||||
category: marketplace.category,
|
||||
createdAt: marketplace.createdAt,
|
||||
updatedAt: marketplace.updatedAt,
|
||||
}
|
||||
|
||||
// Add state if requested
|
||||
const selectFields = includeState ? { ...baseFields, state: marketplace.state } : baseFields
|
||||
|
||||
// Determine which sections to fetch
|
||||
const sections = sectionParam ? sectionParam.split(',') : ['popular', 'recent', 'byCategory']
|
||||
|
||||
// Get popular items if requested
|
||||
if (sections.includes('popular')) {
|
||||
result.popular = await db
|
||||
.select(selectFields)
|
||||
.from(marketplace)
|
||||
.orderBy(desc(marketplace.views))
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
// Get recent items if requested
|
||||
if (sections.includes('recent')) {
|
||||
result.recent = await db
|
||||
.select(selectFields)
|
||||
.from(marketplace)
|
||||
.orderBy(desc(marketplace.createdAt))
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
// Get categories if requested
|
||||
if (
|
||||
sections.includes('byCategory') ||
|
||||
categoryParam ||
|
||||
sections.some((s) => CATEGORIES.some((c) => c.value === s))
|
||||
) {
|
||||
// Identify all requested categories
|
||||
const requestedCategories = new Set<string>()
|
||||
|
||||
// Add explicitly requested category
|
||||
if (categoryParam) {
|
||||
requestedCategories.add(categoryParam)
|
||||
}
|
||||
|
||||
// Add categories from sections parameter
|
||||
sections.forEach((section) => {
|
||||
if (CATEGORIES.some((c) => c.value === section)) {
|
||||
requestedCategories.add(section)
|
||||
}
|
||||
})
|
||||
|
||||
// Include byCategory section contents if requested
|
||||
if (sections.includes('byCategory')) {
|
||||
CATEGORIES.forEach((c) => requestedCategories.add(c.value))
|
||||
}
|
||||
|
||||
// Log what we're fetching
|
||||
const categoriesToFetch = Array.from(requestedCategories)
|
||||
logger.info(`[${requestId}] Fetching specific categories: ${categoriesToFetch.join(', ')}`)
|
||||
|
||||
// Process each requested category
|
||||
await Promise.all(
|
||||
categoriesToFetch.map(async (categoryValue) => {
|
||||
const categoryItems = await db
|
||||
.select(selectFields)
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.category, categoryValue))
|
||||
.orderBy(desc(marketplace.views))
|
||||
.limit(limit)
|
||||
|
||||
// Always add the category to the result, even if empty
|
||||
result.byCategory[categoryValue] = categoryItems
|
||||
logger.info(
|
||||
`[${requestId}] Category ${categoryValue}: found ${categoryItems.length} items`
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Transform the data if state was included to match the expected format
|
||||
if (includeState) {
|
||||
const transformSection = (section: any[]) => {
|
||||
return section.map((item) => {
|
||||
if ('state' in item) {
|
||||
// Create a new object without the state field, but with workflowState
|
||||
const { state, ...rest } = item
|
||||
return {
|
||||
...rest,
|
||||
workflowState: state,
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
if (result.popular.length > 0) {
|
||||
result.popular = transformSection(result.popular)
|
||||
}
|
||||
|
||||
if (result.recent.length > 0) {
|
||||
result.recent = transformSection(result.recent)
|
||||
}
|
||||
|
||||
Object.keys(result.byCategory).forEach((category) => {
|
||||
if (result.byCategory[category].length > 0) {
|
||||
result.byCategory[category] = transformSection(result.byCategory[category])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Fetched marketplace items${includeState ? ' with state' : ''}`)
|
||||
return NextResponse.json(result)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error fetching marketplace items`, error)
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST handler for incrementing view counts
|
||||
*
|
||||
* Request body:
|
||||
* - id: Marketplace entry ID to increment view count for
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { id } = body
|
||||
|
||||
if (!id) {
|
||||
return createErrorResponse('Marketplace ID is required', 400)
|
||||
}
|
||||
|
||||
// Find the marketplace entry
|
||||
const marketplaceEntry = await db
|
||||
.select({
|
||||
id: marketplace.id,
|
||||
})
|
||||
.from(marketplace)
|
||||
.where(eq(marketplace.id, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found with ID: ${id}`)
|
||||
return createErrorResponse('Marketplace entry not found', 404)
|
||||
}
|
||||
|
||||
// Increment the view count
|
||||
await db
|
||||
.update(marketplace)
|
||||
.set({
|
||||
views: sql`${marketplace.views} + 1`,
|
||||
})
|
||||
.where(eq(marketplace.id, id))
|
||||
|
||||
logger.info(`[${requestId}] Incremented view count for marketplace entry: ${id}`)
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
})
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error incrementing view count`, error)
|
||||
return createErrorResponse(`Failed to track view: ${error.message}`, 500)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { member, permissions, user, workspace, workspaceMember } from '@/db/schema'
|
||||
import { member, permissions, user, workspace } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('OrganizationWorkspacesAPI')
|
||||
|
||||
@@ -116,10 +116,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
ownerId: workspace.ownerId,
|
||||
createdAt: workspace.createdAt,
|
||||
isOwner: eq(workspace.ownerId, memberId),
|
||||
permissionType: permissions.permissionType,
|
||||
joinedAt: workspaceMember.joinedAt,
|
||||
createdAt: permissions.createdAt,
|
||||
})
|
||||
.from(workspace)
|
||||
.leftJoin(
|
||||
@@ -130,10 +129,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
eq(permissions.userId, memberId)
|
||||
)
|
||||
)
|
||||
.leftJoin(
|
||||
workspaceMember,
|
||||
and(eq(workspaceMember.workspaceId, workspace.id), eq(workspaceMember.userId, memberId))
|
||||
)
|
||||
.where(
|
||||
or(
|
||||
// Member owns the workspace
|
||||
@@ -148,7 +143,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
name: workspace.name,
|
||||
isOwner: workspace.isOwner,
|
||||
permission: workspace.permissionType,
|
||||
joinedAt: workspace.joinedAt,
|
||||
joinedAt: workspace.createdAt,
|
||||
createdAt: workspace.createdAt,
|
||||
}))
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { invitation, member, permissions, workspaceInvitation, workspaceMember } from '@/db/schema'
|
||||
import { invitation, member, permissions, workspaceInvitation } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('OrganizationInvitationAcceptance')
|
||||
|
||||
@@ -135,18 +135,6 @@ export async function GET(req: NextRequest) {
|
||||
wsInvitation.expiresAt &&
|
||||
new Date().toISOString() <= wsInvitation.expiresAt.toISOString()
|
||||
) {
|
||||
// Check if user isn't already a member of the workspace
|
||||
const existingWorkspaceMember = await tx
|
||||
.select()
|
||||
.from(workspaceMember)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, wsInvitation.workspaceId),
|
||||
eq(workspaceMember.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
// Check if user doesn't already have permissions on the workspace
|
||||
const existingPermission = await tx
|
||||
.select()
|
||||
@@ -160,17 +148,7 @@ export async function GET(req: NextRequest) {
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (existingWorkspaceMember.length === 0 && existingPermission.length === 0) {
|
||||
// Add user as workspace member
|
||||
await tx.insert(workspaceMember).values({
|
||||
id: randomUUID(),
|
||||
workspaceId: wsInvitation.workspaceId,
|
||||
userId: session.user.id,
|
||||
role: wsInvitation.role,
|
||||
joinedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
if (existingPermission.length === 0) {
|
||||
// Add workspace permissions
|
||||
await tx.insert(permissions).values({
|
||||
id: randomUUID(),
|
||||
@@ -311,17 +289,6 @@ export async function POST(req: NextRequest) {
|
||||
wsInvitation.expiresAt &&
|
||||
new Date().toISOString() <= wsInvitation.expiresAt.toISOString()
|
||||
) {
|
||||
const existingWorkspaceMember = await tx
|
||||
.select()
|
||||
.from(workspaceMember)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, wsInvitation.workspaceId),
|
||||
eq(workspaceMember.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
const existingPermission = await tx
|
||||
.select()
|
||||
.from(permissions)
|
||||
@@ -334,16 +301,7 @@ export async function POST(req: NextRequest) {
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (existingWorkspaceMember.length === 0 && existingPermission.length === 0) {
|
||||
await tx.insert(workspaceMember).values({
|
||||
id: randomUUID(),
|
||||
workspaceId: wsInvitation.workspaceId,
|
||||
userId: session.user.id,
|
||||
role: wsInvitation.role,
|
||||
joinedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
if (existingPermission.length === 0) {
|
||||
await tx.insert(permissions).values({
|
||||
id: randomUUID(),
|
||||
userId: session.user.id,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
generateCronExpression,
|
||||
getScheduleTimeValues,
|
||||
getSubBlockValue,
|
||||
validateCronExpression,
|
||||
} from '@/lib/schedules/utils'
|
||||
import { db } from '@/db'
|
||||
import { workflowSchedule } from '@/db/schema'
|
||||
@@ -192,6 +193,18 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
cronExpression = generateCronExpression(defaultScheduleType, scheduleValues)
|
||||
|
||||
// Additional validation for custom cron expressions
|
||||
if (defaultScheduleType === 'custom' && cronExpression) {
|
||||
const validation = validateCronExpression(cronExpression)
|
||||
if (!validation.isValid) {
|
||||
logger.error(`[${requestId}] Invalid cron expression: ${validation.error}`)
|
||||
return NextResponse.json(
|
||||
{ error: `Invalid cron expression: ${validation.error}` },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
nextRunAt = calculateNextRunTime(defaultScheduleType, scheduleValues)
|
||||
|
||||
logger.debug(
|
||||
|
||||
65
apps/sim/app/api/templates/[id]/route.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { templates } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('TemplateByIdAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const revalidate = 0
|
||||
|
||||
// GET /api/templates/[id] - Retrieve a single template by ID
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized template access attempt for ID: ${id}`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
logger.debug(`[${requestId}] Fetching template: ${id}`)
|
||||
|
||||
// Fetch the template by ID
|
||||
const result = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
|
||||
|
||||
if (result.length === 0) {
|
||||
logger.warn(`[${requestId}] Template not found: ${id}`)
|
||||
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const template = result[0]
|
||||
|
||||
// Increment the view count
|
||||
try {
|
||||
await db
|
||||
.update(templates)
|
||||
.set({
|
||||
views: sql`${templates.views} + 1`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(templates.id, id))
|
||||
|
||||
logger.debug(`[${requestId}] Incremented view count for template: ${id}`)
|
||||
} catch (viewError) {
|
||||
// Log the error but don't fail the request
|
||||
logger.warn(`[${requestId}] Failed to increment view count for template: ${id}`, viewError)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully retrieved template: ${id}`)
|
||||
|
||||
return NextResponse.json({
|
||||
data: {
|
||||
...template,
|
||||
views: template.views + 1, // Return the incremented view count
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error fetching template: ${id}`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
173
apps/sim/app/api/templates/[id]/star/route.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { templateStars, templates } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('TemplateStarAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const revalidate = 0
|
||||
|
||||
// GET /api/templates/[id]/star - Check if user has starred this template
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized star check attempt for template: ${id}`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`[${requestId}] Checking star status for template: ${id}, user: ${session.user.id}`
|
||||
)
|
||||
|
||||
// Check if the user has starred this template
|
||||
const starRecord = await db
|
||||
.select({ id: templateStars.id })
|
||||
.from(templateStars)
|
||||
.where(and(eq(templateStars.templateId, id), eq(templateStars.userId, session.user.id)))
|
||||
.limit(1)
|
||||
|
||||
const isStarred = starRecord.length > 0
|
||||
|
||||
logger.info(`[${requestId}] Star status checked: ${isStarred} for template: ${id}`)
|
||||
|
||||
return NextResponse.json({ data: { isStarred } })
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error checking star status for template: ${id}`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/templates/[id]/star - Add a star to the template
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized star attempt for template: ${id}`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
logger.debug(`[${requestId}] Adding star for template: ${id}, user: ${session.user.id}`)
|
||||
|
||||
// Verify the template exists
|
||||
const templateExists = await db
|
||||
.select({ id: templates.id })
|
||||
.from(templates)
|
||||
.where(eq(templates.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (templateExists.length === 0) {
|
||||
logger.warn(`[${requestId}] Template not found: ${id}`)
|
||||
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check if user has already starred this template
|
||||
const existingStar = await db
|
||||
.select({ id: templateStars.id })
|
||||
.from(templateStars)
|
||||
.where(and(eq(templateStars.templateId, id), eq(templateStars.userId, session.user.id)))
|
||||
.limit(1)
|
||||
|
||||
if (existingStar.length > 0) {
|
||||
logger.info(`[${requestId}] Template already starred: ${id}`)
|
||||
return NextResponse.json({ message: 'Template already starred' }, { status: 200 })
|
||||
}
|
||||
|
||||
// Use a transaction to ensure consistency
|
||||
await db.transaction(async (tx) => {
|
||||
// Add the star record
|
||||
await tx.insert(templateStars).values({
|
||||
id: uuidv4(),
|
||||
userId: session.user.id,
|
||||
templateId: id,
|
||||
starredAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
// Increment the star count
|
||||
await tx
|
||||
.update(templates)
|
||||
.set({
|
||||
stars: sql`${templates.stars} + 1`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(templates.id, id))
|
||||
})
|
||||
|
||||
logger.info(`[${requestId}] Successfully starred template: ${id}`)
|
||||
return NextResponse.json({ message: 'Template starred successfully' }, { status: 201 })
|
||||
} catch (error: any) {
|
||||
// Handle unique constraint violations gracefully
|
||||
if (error.code === '23505') {
|
||||
logger.info(`[${requestId}] Duplicate star attempt for template: ${id}`)
|
||||
return NextResponse.json({ message: 'Template already starred' }, { status: 200 })
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Error starring template: ${id}`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/templates/[id]/star - Remove a star from the template
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized unstar attempt for template: ${id}`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
logger.debug(`[${requestId}] Removing star for template: ${id}, user: ${session.user.id}`)
|
||||
|
||||
// Check if the star exists
|
||||
const existingStar = await db
|
||||
.select({ id: templateStars.id })
|
||||
.from(templateStars)
|
||||
.where(and(eq(templateStars.templateId, id), eq(templateStars.userId, session.user.id)))
|
||||
.limit(1)
|
||||
|
||||
if (existingStar.length === 0) {
|
||||
logger.info(`[${requestId}] No star found to remove for template: ${id}`)
|
||||
return NextResponse.json({ message: 'Template not starred' }, { status: 200 })
|
||||
}
|
||||
|
||||
// Use a transaction to ensure consistency
|
||||
await db.transaction(async (tx) => {
|
||||
// Remove the star record
|
||||
await tx
|
||||
.delete(templateStars)
|
||||
.where(and(eq(templateStars.templateId, id), eq(templateStars.userId, session.user.id)))
|
||||
|
||||
// Decrement the star count (prevent negative values)
|
||||
await tx
|
||||
.update(templates)
|
||||
.set({
|
||||
stars: sql`GREATEST(${templates.stars} - 1, 0)`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(templates.id, id))
|
||||
})
|
||||
|
||||
logger.info(`[${requestId}] Successfully unstarred template: ${id}`)
|
||||
return NextResponse.json({ message: 'Template unstarred successfully' }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error unstarring template: ${id}`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
204
apps/sim/app/api/templates/[id]/use/route.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { templates, workflow, workflowBlocks, workflowEdges } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('TemplateUseAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const revalidate = 0
|
||||
|
||||
// POST /api/templates/[id]/use - Use a template (increment views and create workflow)
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized use attempt for template: ${id}`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get workspace ID from request body
|
||||
const body = await request.json()
|
||||
const { workspaceId } = body
|
||||
|
||||
if (!workspaceId) {
|
||||
logger.warn(`[${requestId}] Missing workspaceId in request body`)
|
||||
return NextResponse.json({ error: 'Workspace ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`[${requestId}] Using template: ${id}, user: ${session.user.id}, workspace: ${workspaceId}`
|
||||
)
|
||||
|
||||
// Get the template with its data
|
||||
const template = await db
|
||||
.select({
|
||||
id: templates.id,
|
||||
name: templates.name,
|
||||
description: templates.description,
|
||||
state: templates.state,
|
||||
color: templates.color,
|
||||
})
|
||||
.from(templates)
|
||||
.where(eq(templates.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (template.length === 0) {
|
||||
logger.warn(`[${requestId}] Template not found: ${id}`)
|
||||
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const templateData = template[0]
|
||||
|
||||
// Create a new workflow ID
|
||||
const newWorkflowId = uuidv4()
|
||||
|
||||
// Use a transaction to ensure consistency
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// Increment the template views
|
||||
await tx
|
||||
.update(templates)
|
||||
.set({
|
||||
views: sql`${templates.views} + 1`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(templates.id, id))
|
||||
|
||||
const now = new Date()
|
||||
|
||||
// Create a new workflow from the template
|
||||
const newWorkflow = await tx
|
||||
.insert(workflow)
|
||||
.values({
|
||||
id: newWorkflowId,
|
||||
workspaceId: workspaceId,
|
||||
name: `${templateData.name} (copy)`,
|
||||
description: templateData.description,
|
||||
state: templateData.state,
|
||||
color: templateData.color,
|
||||
userId: session.user.id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
lastSynced: now,
|
||||
})
|
||||
.returning({ id: workflow.id })
|
||||
|
||||
// Create workflow_blocks entries from the template state
|
||||
const templateState = templateData.state as any
|
||||
if (templateState?.blocks) {
|
||||
// Create a mapping from old block IDs to new block IDs for reference updates
|
||||
const blockIdMap = new Map<string, string>()
|
||||
|
||||
const blockEntries = Object.values(templateState.blocks).map((block: any) => {
|
||||
const newBlockId = uuidv4()
|
||||
blockIdMap.set(block.id, newBlockId)
|
||||
|
||||
return {
|
||||
id: newBlockId,
|
||||
workflowId: newWorkflowId,
|
||||
type: block.type,
|
||||
name: block.name,
|
||||
positionX: block.position?.x?.toString() || '0',
|
||||
positionY: block.position?.y?.toString() || '0',
|
||||
enabled: block.enabled !== false,
|
||||
horizontalHandles: block.horizontalHandles !== false,
|
||||
isWide: block.isWide || false,
|
||||
advancedMode: block.advancedMode || false,
|
||||
height: block.height?.toString() || '0',
|
||||
subBlocks: block.subBlocks || {},
|
||||
outputs: block.outputs || {},
|
||||
data: block.data || {},
|
||||
parentId: block.parentId ? blockIdMap.get(block.parentId) || null : null,
|
||||
extent: block.extent || null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
})
|
||||
|
||||
// Create edge entries with new IDs
|
||||
const edgeEntries = (templateState.edges || []).map((edge: any) => ({
|
||||
id: uuidv4(),
|
||||
workflowId: newWorkflowId,
|
||||
sourceBlockId: blockIdMap.get(edge.source) || edge.source,
|
||||
targetBlockId: blockIdMap.get(edge.target) || edge.target,
|
||||
sourceHandle: edge.sourceHandle || null,
|
||||
targetHandle: edge.targetHandle || null,
|
||||
createdAt: now,
|
||||
}))
|
||||
|
||||
// Update the workflow state with new block IDs
|
||||
const updatedState = { ...templateState }
|
||||
if (updatedState.blocks) {
|
||||
const newBlocks: any = {}
|
||||
Object.entries(updatedState.blocks).forEach(([oldId, blockData]: [string, any]) => {
|
||||
const newId = blockIdMap.get(oldId)
|
||||
if (newId) {
|
||||
newBlocks[newId] = {
|
||||
...blockData,
|
||||
id: newId,
|
||||
}
|
||||
}
|
||||
})
|
||||
updatedState.blocks = newBlocks
|
||||
}
|
||||
|
||||
// Update edges to use new block IDs
|
||||
if (updatedState.edges) {
|
||||
updatedState.edges = updatedState.edges.map((edge: any) => ({
|
||||
...edge,
|
||||
id: uuidv4(),
|
||||
source: blockIdMap.get(edge.source) || edge.source,
|
||||
target: blockIdMap.get(edge.target) || edge.target,
|
||||
}))
|
||||
}
|
||||
|
||||
// Update the workflow with the corrected state
|
||||
await tx.update(workflow).set({ state: updatedState }).where(eq(workflow.id, newWorkflowId))
|
||||
|
||||
// Insert blocks and edges
|
||||
if (blockEntries.length > 0) {
|
||||
await tx.insert(workflowBlocks).values(blockEntries)
|
||||
}
|
||||
if (edgeEntries.length > 0) {
|
||||
await tx.insert(workflowEdges).values(edgeEntries)
|
||||
}
|
||||
}
|
||||
|
||||
return newWorkflow[0]
|
||||
})
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully used template: ${id}, created workflow: ${newWorkflowId}, database returned: ${result.id}`
|
||||
)
|
||||
|
||||
// Verify the workflow was actually created
|
||||
const verifyWorkflow = await db
|
||||
.select({ id: workflow.id })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, newWorkflowId))
|
||||
.limit(1)
|
||||
|
||||
if (verifyWorkflow.length === 0) {
|
||||
logger.error(`[${requestId}] Workflow was not created properly: ${newWorkflowId}`)
|
||||
return NextResponse.json({ error: 'Failed to create workflow' }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: 'Template used successfully',
|
||||
workflowId: newWorkflowId,
|
||||
workspaceId: workspaceId,
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error using template: ${id}`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
260
apps/sim/app/api/templates/route.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { and, desc, eq, ilike, or, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { templateStars, templates, workflow } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('TemplatesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const revalidate = 0
|
||||
|
||||
// Function to sanitize sensitive data from workflow state
|
||||
function sanitizeWorkflowState(state: any): any {
|
||||
const sanitizedState = JSON.parse(JSON.stringify(state)) // Deep clone
|
||||
|
||||
if (sanitizedState.blocks) {
|
||||
Object.values(sanitizedState.blocks).forEach((block: any) => {
|
||||
if (block.subBlocks) {
|
||||
Object.entries(block.subBlocks).forEach(([key, subBlock]: [string, any]) => {
|
||||
// Clear OAuth credentials and API keys using regex patterns
|
||||
if (
|
||||
/credential|oauth|api[_-]?key|token|secret|auth|password|bearer/i.test(key) ||
|
||||
/credential|oauth|api[_-]?key|token|secret|auth|password|bearer/i.test(
|
||||
subBlock.type || ''
|
||||
) ||
|
||||
/credential|oauth|api[_-]?key|token|secret|auth|password|bearer/i.test(
|
||||
subBlock.value || ''
|
||||
)
|
||||
) {
|
||||
subBlock.value = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Also clear from data field if present
|
||||
if (block.data) {
|
||||
Object.entries(block.data).forEach(([key, value]: [string, any]) => {
|
||||
if (/credential|oauth|api[_-]?key|token|secret|auth|password|bearer/i.test(key)) {
|
||||
block.data[key] = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return sanitizedState
|
||||
}
|
||||
|
||||
// Schema for creating a template
|
||||
const CreateTemplateSchema = z.object({
|
||||
workflowId: z.string().min(1, 'Workflow ID is required'),
|
||||
name: z.string().min(1, 'Name is required').max(100, 'Name must be less than 100 characters'),
|
||||
description: z
|
||||
.string()
|
||||
.min(1, 'Description is required')
|
||||
.max(500, 'Description must be less than 500 characters'),
|
||||
author: z
|
||||
.string()
|
||||
.min(1, 'Author is required')
|
||||
.max(100, 'Author must be less than 100 characters'),
|
||||
category: z.string().min(1, 'Category is required'),
|
||||
icon: z.string().min(1, 'Icon is required'),
|
||||
color: z.string().regex(/^#[0-9A-F]{6}$/i, 'Color must be a valid hex color (e.g., #3972F6)'),
|
||||
state: z.object({
|
||||
blocks: z.record(z.any()),
|
||||
edges: z.array(z.any()),
|
||||
loops: z.record(z.any()),
|
||||
parallels: z.record(z.any()),
|
||||
}),
|
||||
})
|
||||
|
||||
// Schema for query parameters
|
||||
const QueryParamsSchema = z.object({
|
||||
category: z.string().optional(),
|
||||
limit: z.coerce.number().optional().default(50),
|
||||
offset: z.coerce.number().optional().default(0),
|
||||
search: z.string().optional(),
|
||||
})
|
||||
|
||||
// GET /api/templates - Retrieve templates
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized templates access attempt`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const params = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries()))
|
||||
|
||||
logger.debug(`[${requestId}] Fetching templates with params:`, params)
|
||||
|
||||
// Build query conditions
|
||||
const conditions = []
|
||||
|
||||
// Apply category filter if provided
|
||||
if (params.category) {
|
||||
conditions.push(eq(templates.category, params.category))
|
||||
}
|
||||
|
||||
// Apply search filter if provided
|
||||
if (params.search) {
|
||||
const searchTerm = `%${params.search}%`
|
||||
conditions.push(
|
||||
or(ilike(templates.name, searchTerm), ilike(templates.description, searchTerm))
|
||||
)
|
||||
}
|
||||
|
||||
// Combine conditions
|
||||
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined
|
||||
|
||||
// Apply ordering, limit, and offset with star information
|
||||
const results = await db
|
||||
.select({
|
||||
id: templates.id,
|
||||
workflowId: templates.workflowId,
|
||||
userId: templates.userId,
|
||||
name: templates.name,
|
||||
description: templates.description,
|
||||
author: templates.author,
|
||||
views: templates.views,
|
||||
stars: templates.stars,
|
||||
color: templates.color,
|
||||
icon: templates.icon,
|
||||
category: templates.category,
|
||||
state: templates.state,
|
||||
createdAt: templates.createdAt,
|
||||
updatedAt: templates.updatedAt,
|
||||
isStarred: sql<boolean>`CASE WHEN ${templateStars.id} IS NOT NULL THEN true ELSE false END`,
|
||||
})
|
||||
.from(templates)
|
||||
.leftJoin(
|
||||
templateStars,
|
||||
and(eq(templateStars.templateId, templates.id), eq(templateStars.userId, session.user.id))
|
||||
)
|
||||
.where(whereCondition)
|
||||
.orderBy(desc(templates.views), desc(templates.createdAt))
|
||||
.limit(params.limit)
|
||||
.offset(params.offset)
|
||||
|
||||
// Get total count for pagination
|
||||
const totalCount = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(templates)
|
||||
.where(whereCondition)
|
||||
|
||||
const total = totalCount[0]?.count || 0
|
||||
|
||||
logger.info(`[${requestId}] Successfully retrieved ${results.length} templates`)
|
||||
|
||||
return NextResponse.json({
|
||||
data: results,
|
||||
pagination: {
|
||||
total,
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
page: Math.floor(params.offset / params.limit) + 1,
|
||||
totalPages: Math.ceil(total / params.limit),
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid query parameters`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid query parameters', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Error fetching templates`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/templates - Create a new template
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized template creation attempt`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const data = CreateTemplateSchema.parse(body)
|
||||
|
||||
logger.debug(`[${requestId}] Creating template:`, {
|
||||
name: data.name,
|
||||
category: data.category,
|
||||
workflowId: data.workflowId,
|
||||
})
|
||||
|
||||
// Verify the workflow exists and belongs to the user
|
||||
const workflowExists = await db
|
||||
.select({ id: workflow.id })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, data.workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (workflowExists.length === 0) {
|
||||
logger.warn(`[${requestId}] Workflow not found: ${data.workflowId}`)
|
||||
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Create the template
|
||||
const templateId = uuidv4()
|
||||
const now = new Date()
|
||||
|
||||
// Sanitize the workflow state to remove sensitive credentials
|
||||
const sanitizedState = sanitizeWorkflowState(data.state)
|
||||
|
||||
const newTemplate = {
|
||||
id: templateId,
|
||||
workflowId: data.workflowId,
|
||||
userId: session.user.id,
|
||||
name: data.name,
|
||||
description: data.description || null,
|
||||
author: data.author,
|
||||
views: 0,
|
||||
stars: 0,
|
||||
color: data.color,
|
||||
icon: data.icon,
|
||||
category: data.category,
|
||||
state: sanitizedState,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
await db.insert(templates).values(newTemplate)
|
||||
|
||||
logger.info(`[${requestId}] Successfully created template: ${templateId}`)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
id: templateId,
|
||||
message: 'Template created successfully',
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid template data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid template data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Error creating template`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,10 @@ const logger = createLogger('UserSettingsAPI')
|
||||
|
||||
const SettingsSchema = z.object({
|
||||
theme: z.enum(['system', 'light', 'dark']).optional(),
|
||||
debugMode: z.boolean().optional(),
|
||||
autoConnect: z.boolean().optional(),
|
||||
autoFillEnvVars: z.boolean().optional(),
|
||||
autoPan: z.boolean().optional(),
|
||||
consoleExpandedByDefault: z.boolean().optional(),
|
||||
telemetryEnabled: z.boolean().optional(),
|
||||
telemetryNotifiedUser: z.boolean().optional(),
|
||||
emailPreferences: z
|
||||
@@ -30,10 +30,10 @@ const SettingsSchema = z.object({
|
||||
// Default settings values
|
||||
const defaultSettings = {
|
||||
theme: 'system',
|
||||
debugMode: false,
|
||||
autoConnect: true,
|
||||
autoFillEnvVars: true,
|
||||
autoPan: true,
|
||||
consoleExpandedByDefault: true,
|
||||
telemetryEnabled: true,
|
||||
telemetryNotifiedUser: false,
|
||||
emailPreferences: {},
|
||||
@@ -64,10 +64,10 @@ export async function GET() {
|
||||
{
|
||||
data: {
|
||||
theme: userSettings.theme,
|
||||
debugMode: userSettings.debugMode,
|
||||
autoConnect: userSettings.autoConnect,
|
||||
autoFillEnvVars: userSettings.autoFillEnvVars,
|
||||
autoPan: userSettings.autoPan,
|
||||
consoleExpandedByDefault: userSettings.consoleExpandedByDefault,
|
||||
telemetryEnabled: userSettings.telemetryEnabled,
|
||||
telemetryNotifiedUser: userSettings.telemetryNotifiedUser,
|
||||
emailPreferences: userSettings.emailPreferences ?? {},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import crypto from 'crypto'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
import { db } from '@/db'
|
||||
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'
|
||||
import type { LoopConfig, ParallelConfig, WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
@@ -24,15 +25,13 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(
|
||||
`[${requestId}] Unauthorized workflow duplication attempt for ${sourceWorkflowId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized workflow duplication attempt for ${sourceWorkflowId}`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { name, description, color, workspaceId, folderId } = DuplicateRequestSchema.parse(body)
|
||||
|
||||
@@ -46,19 +45,43 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
|
||||
// Duplicate workflow and all related data in a transaction
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// First verify the source workflow exists and user has access
|
||||
// First verify the source workflow exists
|
||||
const sourceWorkflow = await tx
|
||||
.select()
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.id, sourceWorkflowId), eq(workflow.userId, session.user.id)))
|
||||
.where(eq(workflow.id, sourceWorkflowId))
|
||||
.limit(1)
|
||||
|
||||
if (sourceWorkflow.length === 0) {
|
||||
throw new Error('Source workflow not found or access denied')
|
||||
throw new Error('Source workflow not found')
|
||||
}
|
||||
|
||||
const source = sourceWorkflow[0]
|
||||
|
||||
// Check if user has permission to access the source workflow
|
||||
let canAccessSource = false
|
||||
|
||||
// Case 1: User owns the workflow
|
||||
if (source.userId === session.user.id) {
|
||||
canAccessSource = true
|
||||
}
|
||||
|
||||
// Case 2: User has admin or write permission in the source workspace
|
||||
if (!canAccessSource && source.workspaceId) {
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
source.workspaceId
|
||||
)
|
||||
if (userPermission === 'admin' || userPermission === 'write') {
|
||||
canAccessSource = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!canAccessSource) {
|
||||
throw new Error('Source workflow not found or access denied')
|
||||
}
|
||||
|
||||
// Create the new workflow first (required for foreign key constraints)
|
||||
await tx.insert(workflow).values({
|
||||
id: newWorkflowId,
|
||||
@@ -346,9 +369,18 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
|
||||
return NextResponse.json(result, { status: 201 })
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Source workflow not found or access denied') {
|
||||
logger.warn(`[${requestId}] Source workflow ${sourceWorkflowId} not found or access denied`)
|
||||
return NextResponse.json({ error: 'Source workflow not found' }, { status: 404 })
|
||||
if (error instanceof Error) {
|
||||
if (error.message === 'Source workflow not found') {
|
||||
logger.warn(`[${requestId}] Source workflow ${sourceWorkflowId} not found`)
|
||||
return NextResponse.json({ error: 'Source workflow not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (error.message === 'Source workflow not found or access denied') {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${session.user.id} denied access to source workflow ${sourceWorkflowId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
|
||||
@@ -33,6 +33,20 @@ const EnvVarsSchema = z.record(z.string())
|
||||
// Use a combination of workflow ID and request ID to allow concurrent executions with different inputs
|
||||
const runningExecutions = new Set<string>()
|
||||
|
||||
// Utility function to filter out logs and workflowConnections from API response
|
||||
function createFilteredResult(result: any) {
|
||||
return {
|
||||
...result,
|
||||
logs: undefined,
|
||||
metadata: result.metadata
|
||||
? {
|
||||
...result.metadata,
|
||||
workflowConnections: undefined,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Custom error class for usage limit exceeded
|
||||
class UsageLimitError extends Error {
|
||||
statusCode: number
|
||||
@@ -358,7 +372,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
return createHttpResponseFromBlock(result)
|
||||
}
|
||||
|
||||
return createSuccessResponse(result)
|
||||
// Filter out logs and workflowConnections from the API response
|
||||
const filteredResult = createFilteredResult(result)
|
||||
|
||||
return createSuccessResponse(filteredResult)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error executing workflow: ${id}`, error)
|
||||
|
||||
@@ -418,7 +435,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
return createHttpResponseFromBlock(result)
|
||||
}
|
||||
|
||||
return createSuccessResponse(result)
|
||||
// Filter out logs and workflowConnections from the API response
|
||||
const filteredResult = createFilteredResult(result)
|
||||
|
||||
return createSuccessResponse(filteredResult)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error executing workflow: ${id}`, error)
|
||||
|
||||
|
||||
@@ -79,23 +79,15 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
variablesRecord[variable.id] = variable
|
||||
})
|
||||
|
||||
// Get existing variables to merge with the incoming ones
|
||||
const existingVariables = (workflowRecord[0].variables as Record<string, Variable>) || {}
|
||||
|
||||
// Create a timestamp based on the current request
|
||||
|
||||
// Merge variables: Keep existing ones and update/add new ones
|
||||
// This prevents variables from being deleted during race conditions
|
||||
const mergedVariables = {
|
||||
...existingVariables,
|
||||
...variablesRecord,
|
||||
}
|
||||
// Replace variables completely with the incoming ones
|
||||
// The frontend is the source of truth for what variables should exist
|
||||
const updatedVariables = variablesRecord
|
||||
|
||||
// Update workflow with variables
|
||||
await db
|
||||
.update(workflow)
|
||||
.set({
|
||||
variables: mergedVariables,
|
||||
variables: updatedVariables,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(workflow.id, workflowId))
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import crypto from 'crypto'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getUsersWithPermissions, hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
|
||||
import { db } from '@/db'
|
||||
import { permissions, type permissionTypeEnum, workspaceMember } from '@/db/schema'
|
||||
import { permissions, type permissionTypeEnum } from '@/db/schema'
|
||||
|
||||
type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
|
||||
|
||||
@@ -33,18 +34,19 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
}
|
||||
|
||||
// Verify the current user has access to this workspace
|
||||
const userMembership = await db
|
||||
const userPermission = await db
|
||||
.select()
|
||||
.from(workspaceMember)
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, workspaceId),
|
||||
eq(workspaceMember.userId, session.user.id)
|
||||
eq(permissions.entityId, workspaceId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (userMembership.length === 0) {
|
||||
if (userPermission.length === 0) {
|
||||
return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 })
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { workflow, workspaceMember } from '@/db/schema'
|
||||
import { workflow } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('WorkspaceByIdAPI')
|
||||
|
||||
@@ -126,9 +126,6 @@ export async function DELETE(
|
||||
// workflow_schedule, webhook, marketplace, chat, and memory records
|
||||
await tx.delete(workflow).where(eq(workflow.workspaceId, workspaceId))
|
||||
|
||||
// Delete workspace members
|
||||
await tx.delete(workspaceMember).where(eq(workspaceMember.workspaceId, workspaceId))
|
||||
|
||||
// Delete all permissions associated with this workspace
|
||||
await tx
|
||||
.delete(permissions)
|
||||
|
||||
241
apps/sim/app/api/workspaces/invitations/[id]/route.test.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
|
||||
import { db } from '@/db'
|
||||
import { workspaceInvitation } from '@/db/schema'
|
||||
import { DELETE } from './route'
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
getSession: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/permissions/utils', () => ({
|
||||
hasWorkspaceAdminAccess: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/db/schema', () => ({
|
||||
workspaceInvitation: {
|
||||
id: 'id',
|
||||
workspaceId: 'workspaceId',
|
||||
email: 'email',
|
||||
inviterId: 'inviterId',
|
||||
status: 'status',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
eq: vi.fn((a, b) => ({ type: 'eq', a, b })),
|
||||
}))
|
||||
|
||||
describe('DELETE /api/workspaces/invitations/[id]', () => {
|
||||
const mockSession = {
|
||||
user: {
|
||||
id: 'user123',
|
||||
email: 'user@example.com',
|
||||
name: 'Test User',
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
image: null,
|
||||
stripeCustomerId: null,
|
||||
},
|
||||
session: {
|
||||
id: 'session123',
|
||||
token: 'token123',
|
||||
userId: 'user123',
|
||||
expiresAt: new Date(Date.now() + 86400000), // 1 day from now
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
ipAddress: null,
|
||||
userAgent: null,
|
||||
activeOrganizationId: null,
|
||||
},
|
||||
}
|
||||
|
||||
const mockInvitation = {
|
||||
id: 'invitation123',
|
||||
workspaceId: 'workspace456',
|
||||
email: 'invited@example.com',
|
||||
inviterId: 'inviter789',
|
||||
status: 'pending',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return 401 when user is not authenticated', async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(null)
|
||||
|
||||
const req = new NextRequest('http://localhost/api/workspaces/invitations/invitation123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
const params = Promise.resolve({ id: 'invitation123' })
|
||||
const response = await DELETE(req, { params })
|
||||
|
||||
expect(response).toBeInstanceOf(NextResponse)
|
||||
const data = await response.json()
|
||||
expect(response.status).toBe(401)
|
||||
expect(data).toEqual({ error: 'Unauthorized' })
|
||||
})
|
||||
|
||||
it('should return 404 when invitation does not exist', async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(mockSession)
|
||||
|
||||
// Mock invitation not found
|
||||
const mockQuery = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn((callback: (rows: any[]) => any) => {
|
||||
// Simulate empty rows array
|
||||
return Promise.resolve(callback([]))
|
||||
}),
|
||||
}
|
||||
vi.mocked(db.select).mockReturnValue(mockQuery as any)
|
||||
|
||||
const req = new NextRequest('http://localhost/api/workspaces/invitations/non-existent', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
const params = Promise.resolve({ id: 'non-existent' })
|
||||
const response = await DELETE(req, { params })
|
||||
|
||||
expect(response).toBeInstanceOf(NextResponse)
|
||||
const data = await response.json()
|
||||
expect(response.status).toBe(404)
|
||||
expect(data).toEqual({ error: 'Invitation not found' })
|
||||
})
|
||||
|
||||
it('should return 403 when user does not have admin access', async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(mockSession)
|
||||
|
||||
// Mock invitation found
|
||||
const mockQuery = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn((callback: (rows: any[]) => any) => {
|
||||
// Return the first invitation from the array
|
||||
return Promise.resolve(callback([mockInvitation]))
|
||||
}),
|
||||
}
|
||||
vi.mocked(db.select).mockReturnValue(mockQuery as any)
|
||||
|
||||
// Mock user does not have admin access
|
||||
vi.mocked(hasWorkspaceAdminAccess).mockResolvedValue(false)
|
||||
|
||||
const req = new NextRequest('http://localhost/api/workspaces/invitations/invitation123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
const params = Promise.resolve({ id: 'invitation123' })
|
||||
const response = await DELETE(req, { params })
|
||||
|
||||
expect(response).toBeInstanceOf(NextResponse)
|
||||
const data = await response.json()
|
||||
expect(response.status).toBe(403)
|
||||
expect(data).toEqual({ error: 'Insufficient permissions' })
|
||||
expect(hasWorkspaceAdminAccess).toHaveBeenCalledWith('user123', 'workspace456')
|
||||
})
|
||||
|
||||
it('should return 400 when trying to delete non-pending invitation', async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(mockSession)
|
||||
|
||||
// Mock invitation with accepted status
|
||||
const acceptedInvitation = { ...mockInvitation, status: 'accepted' }
|
||||
const mockQuery = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn((callback: (rows: any[]) => any) => {
|
||||
// Return the first invitation from the array
|
||||
return Promise.resolve(callback([acceptedInvitation]))
|
||||
}),
|
||||
}
|
||||
vi.mocked(db.select).mockReturnValue(mockQuery as any)
|
||||
|
||||
// Mock user has admin access
|
||||
vi.mocked(hasWorkspaceAdminAccess).mockResolvedValue(true)
|
||||
|
||||
const req = new NextRequest('http://localhost/api/workspaces/invitations/invitation123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
const params = Promise.resolve({ id: 'invitation123' })
|
||||
const response = await DELETE(req, { params })
|
||||
|
||||
expect(response).toBeInstanceOf(NextResponse)
|
||||
const data = await response.json()
|
||||
expect(response.status).toBe(400)
|
||||
expect(data).toEqual({ error: 'Can only delete pending invitations' })
|
||||
})
|
||||
|
||||
it('should successfully delete pending invitation when user has admin access', async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(mockSession)
|
||||
|
||||
// Mock invitation found
|
||||
const mockQuery = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn((callback: (rows: any[]) => any) => {
|
||||
// Return the first invitation from the array
|
||||
return Promise.resolve(callback([mockInvitation]))
|
||||
}),
|
||||
}
|
||||
vi.mocked(db.select).mockReturnValue(mockQuery as any)
|
||||
|
||||
// Mock user has admin access
|
||||
vi.mocked(hasWorkspaceAdminAccess).mockResolvedValue(true)
|
||||
|
||||
// Mock successful deletion
|
||||
const mockDelete = {
|
||||
where: vi.fn().mockResolvedValue({ rowCount: 1 }),
|
||||
}
|
||||
vi.mocked(db.delete).mockReturnValue(mockDelete as any)
|
||||
|
||||
const req = new NextRequest('http://localhost/api/workspaces/invitations/invitation123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
const params = Promise.resolve({ id: 'invitation123' })
|
||||
const response = await DELETE(req, { params })
|
||||
|
||||
expect(response).toBeInstanceOf(NextResponse)
|
||||
const data = await response.json()
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toEqual({ success: true })
|
||||
expect(db.delete).toHaveBeenCalledWith(workspaceInvitation)
|
||||
expect(mockDelete.where).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return 500 when database error occurs', async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(mockSession)
|
||||
|
||||
// Mock database error
|
||||
const mockQuery = {
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
then: vi.fn().mockRejectedValue(new Error('Database connection failed')),
|
||||
}
|
||||
vi.mocked(db.select).mockReturnValue(mockQuery as any)
|
||||
|
||||
const req = new NextRequest('http://localhost/api/workspaces/invitations/invitation123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
const params = Promise.resolve({ id: 'invitation123' })
|
||||
const response = await DELETE(req, { params })
|
||||
|
||||
expect(response).toBeInstanceOf(NextResponse)
|
||||
const data = await response.json()
|
||||
expect(response.status).toBe(500)
|
||||
expect(data).toEqual({ error: 'Failed to delete invitation' })
|
||||
})
|
||||
})
|
||||
55
apps/sim/app/api/workspaces/invitations/[id]/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
|
||||
import { db } from '@/db'
|
||||
import { workspaceInvitation } from '@/db/schema'
|
||||
|
||||
// DELETE /api/workspaces/invitations/[id] - Delete a workspace invitation
|
||||
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the invitation to delete
|
||||
const invitation = await db
|
||||
.select({
|
||||
id: workspaceInvitation.id,
|
||||
workspaceId: workspaceInvitation.workspaceId,
|
||||
email: workspaceInvitation.email,
|
||||
inviterId: workspaceInvitation.inviterId,
|
||||
status: workspaceInvitation.status,
|
||||
})
|
||||
.from(workspaceInvitation)
|
||||
.where(eq(workspaceInvitation.id, id))
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!invitation) {
|
||||
return NextResponse.json({ error: 'Invitation not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check if current user has admin access to the workspace
|
||||
const hasAdminAccess = await hasWorkspaceAdminAccess(session.user.id, invitation.workspaceId)
|
||||
|
||||
if (!hasAdminAccess) {
|
||||
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Only allow deleting pending invitations
|
||||
if (invitation.status !== 'pending') {
|
||||
return NextResponse.json({ error: 'Can only delete pending invitations' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Delete the invitation
|
||||
await db.delete(workspaceInvitation).where(eq(workspaceInvitation.id, id))
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting workspace invitation:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete invitation' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/env'
|
||||
import { db } from '@/db'
|
||||
import { permissions, user, workspace, workspaceInvitation, workspaceMember } from '@/db/schema'
|
||||
import { permissions, user, workspace, workspaceInvitation } from '@/db/schema'
|
||||
|
||||
// Accept an invitation via token
|
||||
export async function GET(req: NextRequest) {
|
||||
@@ -126,20 +126,21 @@ export async function GET(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user is already a member
|
||||
const existingMembership = await db
|
||||
// Check if user already has permissions for this workspace
|
||||
const existingPermission = await db
|
||||
.select()
|
||||
.from(workspaceMember)
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, invitation.workspaceId),
|
||||
eq(workspaceMember.userId, session.user.id)
|
||||
eq(permissions.entityId, invitation.workspaceId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (existingMembership) {
|
||||
// User is already a member, just mark the invitation as accepted and redirect
|
||||
if (existingPermission) {
|
||||
// User already has permissions, just mark the invitation as accepted and redirect
|
||||
await db
|
||||
.update(workspaceInvitation)
|
||||
.set({
|
||||
@@ -156,35 +157,19 @@ export async function GET(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Add user to workspace, permissions, and mark invitation as accepted in a transaction
|
||||
// Add user permissions and mark invitation as accepted in a transaction
|
||||
await db.transaction(async (tx) => {
|
||||
// Add user to workspace
|
||||
await tx.insert(workspaceMember).values({
|
||||
// Create permissions for the user
|
||||
await tx.insert(permissions).values({
|
||||
id: randomUUID(),
|
||||
workspaceId: invitation.workspaceId,
|
||||
entityType: 'workspace' as const,
|
||||
entityId: invitation.workspaceId,
|
||||
userId: session.user.id,
|
||||
role: invitation.role,
|
||||
joinedAt: new Date(),
|
||||
permissionType: invitation.permissions || 'read',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
// Create permissions for the user
|
||||
const permissionsToInsert = [
|
||||
{
|
||||
id: randomUUID(),
|
||||
entityType: 'workspace' as const,
|
||||
entityId: invitation.workspaceId,
|
||||
userId: session.user.id,
|
||||
permissionType: invitation.permissions || 'read',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
]
|
||||
|
||||
if (permissionsToInsert.length > 0) {
|
||||
await tx.insert(permissions).values(permissionsToInsert)
|
||||
}
|
||||
|
||||
// Mark invitation as accepted
|
||||
await tx
|
||||
.update(workspaceInvitation)
|
||||
|
||||
324
apps/sim/app/api/workspaces/invitations/route.test.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest, mockAuth, mockConsoleLogger } from '@/app/api/__test-utils__/utils'
|
||||
|
||||
describe('Workspace Invitations API Route', () => {
|
||||
const mockWorkspace = { id: 'workspace-1', name: 'Test Workspace' }
|
||||
const mockUser = { id: 'user-1', email: 'test@example.com' }
|
||||
const mockInvitation = { id: 'invitation-1', status: 'pending' }
|
||||
|
||||
let mockDbResults: any[] = []
|
||||
let mockGetSession: any
|
||||
let mockResendSend: any
|
||||
let mockInsertValues: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.resetAllMocks()
|
||||
|
||||
mockDbResults = []
|
||||
mockConsoleLogger()
|
||||
mockAuth(mockUser)
|
||||
|
||||
vi.doMock('crypto', () => ({
|
||||
randomUUID: vi.fn().mockReturnValue('mock-uuid-1234'),
|
||||
}))
|
||||
|
||||
mockGetSession = vi.fn()
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
mockInsertValues = vi.fn().mockResolvedValue(undefined)
|
||||
const mockDbChain = {
|
||||
select: vi.fn().mockReturnThis(),
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
innerJoin: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn().mockReturnThis(),
|
||||
then: vi.fn().mockImplementation((callback: any) => {
|
||||
const result = mockDbResults.shift() || []
|
||||
return callback ? callback(result) : Promise.resolve(result)
|
||||
}),
|
||||
insert: vi.fn().mockReturnThis(),
|
||||
values: mockInsertValues,
|
||||
}
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: mockDbChain,
|
||||
}))
|
||||
|
||||
vi.doMock('@/db/schema', () => ({
|
||||
user: { id: 'user_id', email: 'user_email', name: 'user_name', image: 'user_image' },
|
||||
workspace: { id: 'workspace_id', name: 'workspace_name', ownerId: 'owner_id' },
|
||||
permissions: {
|
||||
userId: 'user_id',
|
||||
entityId: 'entity_id',
|
||||
entityType: 'entity_type',
|
||||
permissionType: 'permission_type',
|
||||
},
|
||||
workspaceInvitation: {
|
||||
id: 'invitation_id',
|
||||
workspaceId: 'workspace_id',
|
||||
email: 'invitation_email',
|
||||
status: 'invitation_status',
|
||||
token: 'invitation_token',
|
||||
inviterId: 'inviter_id',
|
||||
role: 'invitation_role',
|
||||
permissions: 'invitation_permissions',
|
||||
expiresAt: 'expires_at',
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
},
|
||||
permissionTypeEnum: { enumValues: ['admin', 'write', 'read'] as const },
|
||||
}))
|
||||
|
||||
mockResendSend = vi.fn().mockResolvedValue({ id: 'email-id' })
|
||||
vi.doMock('resend', () => ({
|
||||
Resend: vi.fn().mockImplementation(() => ({
|
||||
emails: { send: mockResendSend },
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.doMock('@react-email/render', () => ({
|
||||
render: vi.fn().mockResolvedValue('<html>email content</html>'),
|
||||
}))
|
||||
|
||||
vi.doMock('@/components/emails/workspace-invitation', () => ({
|
||||
WorkspaceInvitationEmail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/env', () => ({
|
||||
env: {
|
||||
RESEND_API_KEY: 'test-resend-key',
|
||||
NEXT_PUBLIC_APP_URL: 'https://test.simstudio.ai',
|
||||
EMAIL_DOMAIN: 'test.simstudio.ai',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/urls/utils', () => ({
|
||||
getEmailDomain: vi.fn().mockReturnValue('simstudio.ai'),
|
||||
}))
|
||||
|
||||
vi.doMock('drizzle-orm', () => ({
|
||||
and: vi.fn().mockImplementation((...args) => ({ type: 'and', conditions: args })),
|
||||
eq: vi.fn().mockImplementation((field, value) => ({ type: 'eq', field, value })),
|
||||
inArray: vi.fn().mockImplementation((field, values) => ({ type: 'inArray', field, values })),
|
||||
}))
|
||||
})
|
||||
|
||||
describe('GET /api/workspaces/invitations', () => {
|
||||
it('should return 401 when user is not authenticated', async () => {
|
||||
mockGetSession.mockResolvedValue(null)
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const req = createMockRequest('GET')
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(data).toEqual({ error: 'Unauthorized' })
|
||||
})
|
||||
|
||||
it('should return empty invitations when user has no workspaces', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
|
||||
mockDbResults = [[], []] // No workspaces, no invitations
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const req = createMockRequest('GET')
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toEqual({ invitations: [] })
|
||||
})
|
||||
|
||||
it('should return invitations for user workspaces', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
|
||||
const mockWorkspaces = [{ id: 'workspace-1' }, { id: 'workspace-2' }]
|
||||
const mockInvitations = [
|
||||
{ id: 'invitation-1', workspaceId: 'workspace-1', email: 'test@example.com' },
|
||||
{ id: 'invitation-2', workspaceId: 'workspace-2', email: 'test2@example.com' },
|
||||
]
|
||||
mockDbResults = [mockWorkspaces, mockInvitations]
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const req = createMockRequest('GET')
|
||||
const response = await GET(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toEqual({ invitations: mockInvitations })
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/workspaces/invitations', () => {
|
||||
it('should return 401 when user is not authenticated', async () => {
|
||||
mockGetSession.mockResolvedValue(null)
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const req = createMockRequest('POST', {
|
||||
workspaceId: 'workspace-1',
|
||||
email: 'test@example.com',
|
||||
})
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
expect(data).toEqual({ error: 'Unauthorized' })
|
||||
})
|
||||
|
||||
it('should return 400 when workspaceId is missing', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const req = createMockRequest('POST', { email: 'test@example.com' })
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data).toEqual({ error: 'Workspace ID and email are required' })
|
||||
})
|
||||
|
||||
it('should return 400 when email is missing', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const req = createMockRequest('POST', { workspaceId: 'workspace-1' })
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data).toEqual({ error: 'Workspace ID and email are required' })
|
||||
})
|
||||
|
||||
it('should return 400 when permission type is invalid', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const req = createMockRequest('POST', {
|
||||
workspaceId: 'workspace-1',
|
||||
email: 'test@example.com',
|
||||
permission: 'invalid-permission',
|
||||
})
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data).toEqual({
|
||||
error: 'Invalid permission: must be one of admin, write, read',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return 403 when user does not have admin permissions', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
|
||||
mockDbResults = [[]] // No admin permissions found
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const req = createMockRequest('POST', {
|
||||
workspaceId: 'workspace-1',
|
||||
email: 'test@example.com',
|
||||
})
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
expect(data).toEqual({ error: 'You need admin permissions to invite users' })
|
||||
})
|
||||
|
||||
it('should return 404 when workspace is not found', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
|
||||
mockDbResults = [
|
||||
[{ permissionType: 'admin' }], // User has admin permissions
|
||||
[], // Workspace not found
|
||||
]
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const req = createMockRequest('POST', {
|
||||
workspaceId: 'workspace-1',
|
||||
email: 'test@example.com',
|
||||
})
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
expect(data).toEqual({ error: 'Workspace not found' })
|
||||
})
|
||||
|
||||
it('should return 400 when user already has workspace access', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
|
||||
mockDbResults = [
|
||||
[{ permissionType: 'admin' }], // User has admin permissions
|
||||
[mockWorkspace], // Workspace exists
|
||||
[mockUser], // User exists
|
||||
[{ permissionType: 'read' }], // User already has access
|
||||
]
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const req = createMockRequest('POST', {
|
||||
workspaceId: 'workspace-1',
|
||||
email: 'test@example.com',
|
||||
})
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data).toEqual({
|
||||
error: 'test@example.com already has access to this workspace',
|
||||
email: 'test@example.com',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return 400 when invitation already exists', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
|
||||
mockDbResults = [
|
||||
[{ permissionType: 'admin' }], // User has admin permissions
|
||||
[mockWorkspace], // Workspace exists
|
||||
[], // User doesn't exist
|
||||
[mockInvitation], // Invitation exists
|
||||
]
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const req = createMockRequest('POST', {
|
||||
workspaceId: 'workspace-1',
|
||||
email: 'test@example.com',
|
||||
})
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(data).toEqual({
|
||||
error: 'test@example.com has already been invited to this workspace',
|
||||
email: 'test@example.com',
|
||||
})
|
||||
})
|
||||
|
||||
it('should successfully create invitation and send email', async () => {
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123', name: 'Test User', email: 'sender@example.com' },
|
||||
})
|
||||
mockDbResults = [
|
||||
[{ permissionType: 'admin' }], // User has admin permissions
|
||||
[mockWorkspace], // Workspace exists
|
||||
[], // User doesn't exist
|
||||
[], // No existing invitation
|
||||
]
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const req = createMockRequest('POST', {
|
||||
workspaceId: 'workspace-1',
|
||||
email: 'test@example.com',
|
||||
permission: 'read',
|
||||
})
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
expect(data.invitation).toBeDefined()
|
||||
expect(data.invitation.email).toBe('test@example.com')
|
||||
expect(data.invitation.permissions).toBe('read')
|
||||
expect(data.invitation.token).toBe('mock-uuid-1234')
|
||||
expect(mockInsertValues).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -10,11 +10,11 @@ import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getEmailDomain } from '@/lib/urls/utils'
|
||||
import { db } from '@/db'
|
||||
import {
|
||||
permissions,
|
||||
type permissionTypeEnum,
|
||||
user,
|
||||
workspace,
|
||||
workspaceInvitation,
|
||||
workspaceMember,
|
||||
} from '@/db/schema'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -33,15 +33,16 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all workspaces where the user is a member (any role)
|
||||
// Get all workspaces where the user has permissions
|
||||
const userWorkspaces = await db
|
||||
.select({ id: workspace.id })
|
||||
.from(workspace)
|
||||
.innerJoin(
|
||||
workspaceMember,
|
||||
permissions,
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, workspace.id),
|
||||
eq(workspaceMember.userId, session.user.id)
|
||||
eq(permissions.entityId, workspace.id),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -89,20 +90,25 @@ export async function POST(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user is authorized to invite to this workspace (must be owner)
|
||||
const membership = await db
|
||||
// Check if user has admin permissions for this workspace
|
||||
const userPermission = await db
|
||||
.select()
|
||||
.from(workspaceMember)
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, workspaceId),
|
||||
eq(workspaceMember.userId, session.user.id)
|
||||
eq(permissions.entityId, workspaceId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.userId, session.user.id),
|
||||
eq(permissions.permissionType, 'admin')
|
||||
)
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!membership) {
|
||||
return NextResponse.json({ error: 'You are not a member of this workspace' }, { status: 403 })
|
||||
if (!userPermission) {
|
||||
return NextResponse.json(
|
||||
{ error: 'You need admin permissions to invite users' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get the workspace details for the email
|
||||
@@ -125,22 +131,23 @@ export async function POST(req: NextRequest) {
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (existingUser) {
|
||||
// Check if the user is already a member of this workspace
|
||||
const existingMembership = await db
|
||||
// Check if the user already has permissions for this workspace
|
||||
const existingPermission = await db
|
||||
.select()
|
||||
.from(workspaceMember)
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, workspaceId),
|
||||
eq(workspaceMember.userId, existingUser.id)
|
||||
eq(permissions.entityId, workspaceId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.userId, existingUser.id)
|
||||
)
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (existingMembership) {
|
||||
if (existingPermission) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `${email} is already a member of this workspace`,
|
||||
error: `${email} already has access to this workspace`,
|
||||
email,
|
||||
},
|
||||
{ status: 400 }
|
||||
@@ -245,14 +252,19 @@ async function sendInvitationEmail({
|
||||
)
|
||||
}
|
||||
|
||||
await resend.emails.send({
|
||||
from: `noreply@${getEmailDomain()}`,
|
||||
const emailDomain = env.EMAIL_DOMAIN || getEmailDomain()
|
||||
const fromAddress = `noreply@${emailDomain}`
|
||||
|
||||
logger.info(`Attempting to send email from ${fromAddress} to ${to}`)
|
||||
|
||||
const result = await resend.emails.send({
|
||||
from: fromAddress,
|
||||
to,
|
||||
subject: `You've been invited to join "${workspaceName}" on Sim Studio`,
|
||||
html: emailHtml,
|
||||
})
|
||||
|
||||
logger.info(`Invitation email sent to ${to}`)
|
||||
logger.info(`Invitation email sent successfully to ${to}`, { result })
|
||||
} catch (error) {
|
||||
logger.error('Error sending invitation email:', error)
|
||||
// Continue even if email fails - the invitation is still created
|
||||
|
||||
@@ -1,79 +1,85 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
|
||||
import { db } from '@/db'
|
||||
import { workspaceMember } from '@/db/schema'
|
||||
import { permissions } from '@/db/schema'
|
||||
|
||||
// DELETE /api/workspaces/members/[id] - Remove a member from a workspace
|
||||
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const { id: userId } = await params
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const membershipId = id
|
||||
|
||||
try {
|
||||
// Get the membership to delete
|
||||
const membership = await db
|
||||
.select({
|
||||
id: workspaceMember.id,
|
||||
workspaceId: workspaceMember.workspaceId,
|
||||
userId: workspaceMember.userId,
|
||||
role: workspaceMember.role,
|
||||
})
|
||||
.from(workspaceMember)
|
||||
.where(eq(workspaceMember.id, membershipId))
|
||||
.then((rows) => rows[0])
|
||||
// Get the workspace ID from the request body or URL
|
||||
const body = await req.json()
|
||||
const workspaceId = body.workspaceId
|
||||
|
||||
if (!membership) {
|
||||
return NextResponse.json({ error: 'Membership not found' }, { status: 404 })
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json({ error: 'Workspace ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if current user is an owner of the workspace or the member being removed
|
||||
const isOwner = await db
|
||||
// Check if the user to be removed actually has permissions for this workspace
|
||||
const userPermission = await db
|
||||
.select()
|
||||
.from(workspaceMember)
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, membership.workspaceId),
|
||||
eq(workspaceMember.userId, session.user.id),
|
||||
eq(workspaceMember.role, 'owner')
|
||||
eq(permissions.userId, userId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId)
|
||||
)
|
||||
)
|
||||
.then((rows) => rows.length > 0)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
const isSelf = membership.userId === session.user.id
|
||||
if (!userPermission) {
|
||||
return NextResponse.json({ error: 'User not found in workspace' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (!isOwner && !isSelf) {
|
||||
// Check if current user has admin access to this workspace
|
||||
const hasAdminAccess = await hasWorkspaceAdminAccess(session.user.id, workspaceId)
|
||||
const isSelf = userId === session.user.id
|
||||
|
||||
if (!hasAdminAccess && !isSelf) {
|
||||
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Prevent removing yourself if you're the owner and the last owner
|
||||
if (isSelf && membership.role === 'owner') {
|
||||
const otherOwners = await db
|
||||
// Prevent removing yourself if you're the last admin
|
||||
if (isSelf && userPermission.permissionType === 'admin') {
|
||||
const otherAdmins = await db
|
||||
.select()
|
||||
.from(workspaceMember)
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, membership.workspaceId),
|
||||
eq(workspaceMember.role, 'owner')
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId),
|
||||
eq(permissions.permissionType, 'admin')
|
||||
)
|
||||
)
|
||||
.then((rows) => rows.filter((row) => row.userId !== session.user.id))
|
||||
|
||||
if (otherOwners.length === 0) {
|
||||
if (otherAdmins.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot remove the last owner from a workspace' },
|
||||
{ error: 'Cannot remove the last admin from a workspace' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the membership
|
||||
await db.delete(workspaceMember).where(eq(workspaceMember.id, membershipId))
|
||||
// Delete the user's permissions for this workspace
|
||||
await db
|
||||
.delete(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.userId, userId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workspaceId)
|
||||
)
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { hasAdminPermission } from '@/lib/permissions/utils'
|
||||
import { db } from '@/db'
|
||||
import { permissions, type permissionTypeEnum, user, workspaceMember } from '@/db/schema'
|
||||
import { permissions, type permissionTypeEnum, user } from '@/db/schema'
|
||||
|
||||
type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
|
||||
|
||||
@@ -71,28 +71,15 @@ export async function POST(req: Request) {
|
||||
)
|
||||
}
|
||||
|
||||
// Use a transaction to ensure data consistency
|
||||
await db.transaction(async (tx) => {
|
||||
// Add user to workspace members table (keeping for compatibility)
|
||||
await tx.insert(workspaceMember).values({
|
||||
id: crypto.randomUUID(),
|
||||
workspaceId,
|
||||
userId: targetUser.id,
|
||||
role: 'member', // Default role for compatibility
|
||||
joinedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
// Create single permission for the new member
|
||||
await tx.insert(permissions).values({
|
||||
id: crypto.randomUUID(),
|
||||
userId: targetUser.id,
|
||||
entityType: 'workspace' as const,
|
||||
entityId: workspaceId,
|
||||
permissionType: permission,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
// Create single permission for the new member
|
||||
await db.insert(permissions).values({
|
||||
id: crypto.randomUUID(),
|
||||
userId: targetUser.id,
|
||||
entityType: 'workspace' as const,
|
||||
entityId: workspaceId,
|
||||
permissionType: permission,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { and, desc, eq, isNull } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { db } from '@/db'
|
||||
import { permissions, workflow, workflowBlocks, workspace, workspaceMember } from '@/db/schema'
|
||||
import { permissions, workflow, workflowBlocks, workspace } from '@/db/schema'
|
||||
|
||||
// Get all workspaces for the current user
|
||||
export async function GET() {
|
||||
@@ -13,18 +13,18 @@ export async function GET() {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get all workspaces where the user is a member with a single join query
|
||||
const memberWorkspaces = await db
|
||||
// Get all workspaces where the user has permissions
|
||||
const userWorkspaces = await db
|
||||
.select({
|
||||
workspace: workspace,
|
||||
role: workspaceMember.role,
|
||||
permissionType: permissions.permissionType,
|
||||
})
|
||||
.from(workspaceMember)
|
||||
.innerJoin(workspace, eq(workspaceMember.workspaceId, workspace.id))
|
||||
.where(eq(workspaceMember.userId, session.user.id))
|
||||
.orderBy(desc(workspaceMember.joinedAt))
|
||||
.from(permissions)
|
||||
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
|
||||
.where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
|
||||
.orderBy(desc(workspace.createdAt))
|
||||
|
||||
if (memberWorkspaces.length === 0) {
|
||||
if (userWorkspaces.length === 0) {
|
||||
// Create a default workspace for the user
|
||||
const defaultWorkspace = await createDefaultWorkspace(session.user.id, session.user.name)
|
||||
|
||||
@@ -35,15 +35,18 @@ export async function GET() {
|
||||
}
|
||||
|
||||
// If user has workspaces but might have orphaned workflows, migrate them
|
||||
await ensureWorkflowsHaveWorkspace(session.user.id, memberWorkspaces[0].workspace.id)
|
||||
await ensureWorkflowsHaveWorkspace(session.user.id, userWorkspaces[0].workspace.id)
|
||||
|
||||
// Format the response
|
||||
const workspaces = memberWorkspaces.map(({ workspace: workspaceDetails, role }) => ({
|
||||
...workspaceDetails,
|
||||
role,
|
||||
}))
|
||||
// Format the response with permission information
|
||||
const workspacesWithPermissions = userWorkspaces.map(
|
||||
({ workspace: workspaceDetails, permissionType }) => ({
|
||||
...workspaceDetails,
|
||||
role: permissionType === 'admin' ? 'owner' : 'member', // Map admin to owner for compatibility
|
||||
permissions: permissionType,
|
||||
})
|
||||
)
|
||||
|
||||
return NextResponse.json({ workspaces })
|
||||
return NextResponse.json({ workspaces: workspacesWithPermissions })
|
||||
}
|
||||
|
||||
// POST /api/workspaces - Create a new workspace
|
||||
@@ -94,13 +97,14 @@ async function createWorkspace(userId: string, name: string) {
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
// Add the user as a member with owner role
|
||||
await tx.insert(workspaceMember).values({
|
||||
// Create admin permissions for the workspace owner
|
||||
await tx.insert(permissions).values({
|
||||
id: crypto.randomUUID(),
|
||||
workspaceId,
|
||||
userId,
|
||||
role: 'owner',
|
||||
joinedAt: now,
|
||||
entityType: 'workspace' as const,
|
||||
entityId: workspaceId,
|
||||
userId: userId,
|
||||
permissionType: 'admin' as const,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
@@ -249,17 +253,6 @@ async function createWorkspace(userId: string, name: string) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Create default permissions for the workspace owner
|
||||
await db.insert(permissions).values({
|
||||
id: crypto.randomUUID(),
|
||||
entityType: 'workspace' as const,
|
||||
entityId: workspaceId,
|
||||
userId: userId,
|
||||
permissionType: 'admin' as const,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
// Return the workspace data directly instead of querying again
|
||||
return {
|
||||
id: workspaceId,
|
||||
|
||||
@@ -269,6 +269,8 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
|
||||
const messageToSend = messageParam ?? inputValue
|
||||
if (!messageToSend.trim() || isLoading) return
|
||||
|
||||
logger.info('Sending message:', { messageToSend, isVoiceInput, conversationId })
|
||||
|
||||
// Reset userHasScrolled when sending a new message
|
||||
setUserHasScrolled(false)
|
||||
|
||||
@@ -305,6 +307,8 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
|
||||
conversationId,
|
||||
}
|
||||
|
||||
logger.info('API payload:', payload)
|
||||
|
||||
const response = await fetch(`/api/chat/${subdomain}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -321,6 +325,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
logger.error('API error response:', errorData)
|
||||
throw new Error(errorData.error || 'Failed to get response')
|
||||
}
|
||||
|
||||
@@ -334,6 +339,8 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
|
||||
? createAudioStreamHandler(streamTextToAudio, DEFAULT_VOICE_SETTINGS.voiceId)
|
||||
: undefined
|
||||
|
||||
logger.info('Starting to handle streamed response:', { shouldPlayAudio })
|
||||
|
||||
await handleStreamedResponse(
|
||||
response,
|
||||
setMessages,
|
||||
@@ -405,6 +412,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
|
||||
// Handle voice transcript from voice-first interface
|
||||
const handleVoiceTranscript = useCallback(
|
||||
(transcript: string) => {
|
||||
logger.info('Received voice transcript:', transcript)
|
||||
handleSendMessage(transcript, true)
|
||||
},
|
||||
[handleSendMessage]
|
||||
|
||||