For development standards, architecture, and code style: See CLAUDE.md
Visual AI workflow builder for creating and deploying agent workflows with drag-and-drop canvas.
- Project Overview
- Tech Stack
- Workflow Concepts
- Block System
- Tools & Integrations
- LLM Providers
- OAuth Services
- MCP Operations
- Available Skills
- Agent Testing & Workflow Development
- Common Patterns
- Troubleshooting
Sim Studio is an open-source visual workflow builder for AI agents. Users design workflows on a canvas by connecting blocks (agents, tools, conditions, loops) with edges defining data flow and execution order.
apps/sim/ # Main Next.js application
├── blocks/ # Block definitions and registry
├── tools/ # Tool definitions (100+ integrations)
├── triggers/ # Webhook trigger definitions
├── providers/ # LLM provider integrations
├── executor/ # Workflow execution engine
└── stores/ # Zustand state management
| Category | Technologies |
|---|---|
| Runtime | TypeScript, React 19, Next.js 16, Bun |
| Data | PostgreSQL, Drizzle ORM, pgvector |
| State | Zustand, React Query |
| Canvas | ReactFlow |
| Realtime | Socket.io |
| Testing | Vitest, @sim/testing |
| Packages | @sim/logger, @sim/db, @sim/testing |
[TRIGGER/INPUT] → [PROCESS] → [OUTPUT]
Reference block outputs using angle brackets with block name:
<BlockName.outputField>
<Start.input> # User input from chat
<Webhook 1.message> # Webhook payload field
<Agent 1.content> # Agent's text response
<API Block.response.data> # Nested API response
Use double braces in text/email blocks:
Subject: Status for {{Webhook 1.tracking_id}}
Body: Result: {{API Block.response.status}}
Reference: <variable.varName> # In block configs
Template: {{variable.varName}} # In text fields
Types: string | number | boolean | object | array | plain
| Category | Purpose |
|---|---|
triggers |
Entry points (start_trigger, generic_webhook, schedule) + service-specific triggers (imap, rss, circleback) |
blocks |
Core logic (agent, condition, router, function) |
tools |
Service integrations (slack, gmail, github) |
interface BlockConfig {
type: string // Block identifier
name: string // Display name
category: 'blocks' | 'tools' | 'triggers'
bgColor: string // Hex color
icon: BlockIcon // SVG component
subBlocks: SubBlockConfig[] // Configuration fields
tools: {
access: string[] // Tool IDs this block uses
config?: { tool: (params) => string }
}
inputs: Record<string, ParamConfig>
outputs: Record<string, OutputFieldDefinition>
triggers?: { enabled: boolean; available: string[] }
}Configuration field types for block settings:
| Type | Purpose |
|---|---|
short-input |
Single line text |
long-input |
Multi-line text |
dropdown |
Select menu |
combobox |
Searchable dropdown |
code |
Code editor |
switch |
Toggle |
oauth-input |
OAuth credential selector |
tool-input |
Tool configuration |
condition-input |
Conditional logic |
file-selector |
File picker (Google Drive, etc.) |
channel-selector |
Channel picker (Slack, Discord) |
Triggers (active): start_trigger, generic_webhook, schedule, imap, rss, circleback
Triggers (legacy, hidden): api_trigger, chat_trigger, input_trigger, manual_trigger
Core: a2a, agent, api, condition, evaluator, function, guardrails, human_in_the_loop, knowledge, mcp, memory, note, parallel_ai, response, router_v2, starter, variables, wait, webhook_request, workflow_input
Core (legacy, hidden): router, workflow
AI/LLM: openai, thinking, vision, image_generator, tts, stt
Communication: slack, discord, microsoft_teams, gmail, outlook, telegram, whatsapp
Productivity: notion, airtable, google_sheets, google_docs, google_drive, asana, linear, jira, trello
Data/Storage: postgresql, mongodb, mysql, dynamodb, s3, supabase, pinecone, qdrant
Search: exa, tavily, serper, duckduckgo, wikipedia
Web: firecrawl, jina, browser_use, stagehand
Note on loop/parallel:
loopandparallelare subflow container types used by workflow state +sim-mcp-update_subflow; they are not registry block keys (parallel_aiis the registered block type).
api | manual | schedule | chat | webhook | mcp | a2a
interface ToolConfig<P, R> {
id: string
name: string
description: string
version: string
oauth?: { required: boolean; provider: OAuthService }
params: Record<string, {
type: string
required?: boolean
visibility?: 'user-or-llm' | 'user-only' | 'llm-only' | 'hidden'
}>
request: {
url: string | ((params) => string)
method: HttpMethod
headers: (params) => Record<string, string>
body?: (params) => any
}
transformResponse?: (response: Response) => Promise<R>
outputs?: Record<string, { type: string; description?: string }>
}Required files:
tools/{service}/- Tool definitions + typesblocks/blocks/{service}.ts- Block configurationcomponents/icons.tsx- Service icontriggers/{service}/- (Optional) Webhook handlers
Register in:
tools/registry.tsblocks/registry.tstriggers/registry.ts(if triggers)
| Provider | ID | Notes |
|---|---|---|
| OpenAI | openai |
GPT-4, GPT-4o, o1 |
| Anthropic | anthropic |
Claude 3.5, Claude 4 |
google |
Gemini models | |
| Vertex AI | vertex |
Google Cloud |
| Azure OpenAI | azure-openai |
Enterprise |
| DeepSeek | deepseek |
|
| xAI | xai |
Grok |
| Groq | groq |
Fast inference |
| Mistral | mistral |
|
| Ollama | ollama |
Local models |
| OpenRouter | openrouter |
Multi-provider |
| vLLM | vllm |
Self-hosted |
| Bedrock | bedrock |
AWS |
| Cerebras | cerebras |
interface ProviderConfig {
id: string
name: string
models: string[]
defaultModel: string
executeRequest: (request: ProviderRequest) => Promise<ProviderResponse>
}| Service | Provider ID | Description |
|---|---|---|
| Gmail | google-email |
Email automation |
| Google Drive | google-drive |
File management |
| Google Docs | google-docs |
Document operations |
| Google Sheets | google-sheets |
Spreadsheet data |
| Google Calendar | google-calendar |
Event scheduling |
| Google Forms | google-forms |
Form responses |
| Vertex AI | vertex-ai |
OAuth for Gemini |
| Service | Provider ID |
|---|---|
| Outlook | microsoft-email |
| Teams | microsoft-teams |
| OneDrive | microsoft-onedrive |
| SharePoint | microsoft-sharepoint |
| Excel | microsoft-excel |
| Planner | microsoft-planner |
slack, discord, github, linear, notion, airtable, jira, confluence, hubspot, salesforce, shopify, spotify, zoom, reddit, linkedin, x, trello, webflow, wealthbox, pipedrive
The sim-mcp server provides programmatic workflow management.
// List all available sim-mcp tools
sim-mcp-list_tools()
// Create a new workflow
sim-mcp-create_workflow({ name, workspaceId, description?, color?, folderId? })
// List workflows
sim-mcp-list_workflows({ workspaceId?, limit?, offset? })
// Get workflow details (verbose=true for full block config)
sim-mcp-get_workflow({ workflowId, verbose? })
// Replace entire workflow state
sim-mcp-replace_workflow_state({ workflowId, state })
// Execute a workflow (uses draft state by default — no deploy needed)
// triggerType: "api" | "manual" | "schedule" | "chat" | "webhook" | "mcp" | "a2a"
sim-mcp-execute_workflow({ workflowId, input?, triggerType?, useDraftState? })// List and filter execution logs
sim-mcp-get_execution_logs({ workspaceId, workflowId?, executionId?, level?, details?, includeTraceSpans?, includeFinalOutput?, limit?, cursor? })
// Get full detail for a specific execution log
sim-mcp-get_execution_log_detail({ logId })// Add blocks with optional edges
sim-mcp-add_blocks({
workflowId,
blocks: [{ type, name, position, data?, enabled?, id? }],
edges?: [{ source, target, sourceHandle?, targetHandle? }]
})
// Get full detail for a single block
sim-mcp-get_block({ workflowId, blockId })
// Remove blocks
sim-mcp-remove_blocks({ workflowId, blockIds: [] })
// Update block name
sim-mcp-update_block_name({ workflowId, blockId, name })
// Toggle enabled state
sim-mcp-toggle_block_enabled({ workflowId, blockId, enabled })
// Update subblock value
sim-mcp-update_subblock({ workflowId, blockId, subblockId, value })
// Set or remove block's parent container (for loop/parallel nesting)
sim-mcp-update_block_parent({ workflowId, blockId, parentId })// Connect blocks
sim-mcp-add_edge({ workflowId, source, target, sourceHandle?, targetHandle? })
// Remove connection
sim-mcp-remove_edge({ workflowId, edgeId })// Add workflow variable
sim-mcp-add_variable({ workflowId, name, type, value, id? })
// Remove variable
sim-mcp-remove_variable({ workflowId, variableId })// Update loop/parallel configuration (create via add_blocks with type "loop"/"parallel")
sim-mcp-update_subflow({ workflowId, id, type?, config? })// List workspace custom tools (metadata only)
sim-mcp-list_custom_tools({ workspaceId?, workflowId? })
// Get full details for a specific custom tool
sim-mcp-get_custom_tool({ toolId, workspaceId?, workflowId? })
// Create/update custom tools
sim-mcp-upsert_custom_tools({
workspaceId,
tools: [{ title, schema, code, id? }]
})// List all skills in a workspace
sim-mcp-list_skills({ workspaceId })
// Get full skill details by ID
sim-mcp-get_skill({ workspaceId, id })
// Create a new skill (name must be kebab-case)
sim-mcp-create_skill({ workspaceId, name, description, content })
// Update an existing skill
sim-mcp-update_skill({ workspaceId, id, name, description, content })
// Delete a skill (does NOT cascade — manually remove from agent blocks)
sim-mcp-delete_skill({ workspaceId, id })Full parameter reference: See
sim-workflowsskill →references/mcp-tools-reference.mdfor detailed parameter tables, return values, and usage examples for all 28 tools.
Invoke with skill tool. Tier indicates when loading is mandatory vs recommended.
Repo-local skills under .agents/skills/ are listed below. Externally loaded
authority skills such as skill-maintainer and skill-creator may still be
loaded when needed, but they are not part of this repository-owned inventory.
If you package a .skill archive for distribution, write it outside the live
tree (for example ./dist/skills) instead of placing it under .agents/skills/.
| Tier | Skill | Use When |
|---|---|---|
| 🔥 Required | sim-workflow-testing |
ANY workflow testing, debugging, trace inspection, or execution verification |
| 🔥 Required | sim-workflows |
Building/modifying workflows via MCP (blocks, edges, variables) |
| 🔥 Required | ikas-api |
Querying ikas e-commerce data (products, orders, customers via GraphQL) |
| ✅ Recommended | db-migrations |
Dual-config Drizzle migration architecture, rebases, watermark debugging, production remediation |
| ✅ Recommended | ecommerce-agent-template |
Creating WhatsApp/chat support agents for ikas companies (product browsing, order lookup, handoff) |
| ✅ Recommended | ikas-products-grouping |
Product discovery tools with category grouping, color/dimension parsing for ikas stores |
| ✅ Recommended | sim-app-development |
Repo-local app development touchpoints and integration checklist outside workflow-only or deployment work |
| ✅ Recommended | sim-runtime |
Starting/monitoring the Sim Studio dev environment |
| ✅ Recommended | sim-self-hosting |
Self-hosting and deployment guidance for npm, Docker Compose, Render, Helm, and environment setup |
| ✅ Recommended | trendyol-api |
Trendyol marketplace Q&A and approved-products API reference |
add-integration / add-block / add-tools / add-trigger |
Slash commands: /add-integration, /add-block, /add-tools, /add-trigger for workflow scaffolding |
🔥 CRITICAL: Load
sim-workflow-testingskill before any testing task — it contains the full protocol, payload templates, verification rules, and block management references.
All custom tools and workflow changes must pass these phases:
Phase 1 — Local JS Validation (catches ~70% of issues at zero cost): Copy custom tool code into standalone JS → mock inputs → run with Node/Bun → test edge cases (empty arrays, null fields, malformed input)
Phase 2 — Isolated Workflow Testing (SNAPSHOT → DISABLE → EXECUTE → VERIFY → RESTORE):
Snapshot block states → disable non-essential blocks → execute with crafted payload (useDraftState: true) → inspect trace_spans in execution logs → always restore, even on failure
Phase 3 — Full Integration Testing: Enable all blocks (disable final sends if destructive) → send realistic payload → verify trace spans show correct routing + outputs → confirm final output format
| Task | Action |
|---|---|
| Test routing only | CONDITION_ONLY profile — disable all except trigger + conditions |
| Test one path | PATH_ISOLATION profile — enable only target-path blocks |
| Check block output | get_execution_logs({ includeTraceSpans: true, details: "full" }) |
| Skip a block | toggle_block_enabled(blockId, enabled=false) |
| Verify tool output | Phase 1 JS first, then Phase 2 isolated execution |
| 🚫 Don't | ✅ Instead |
|---|---|
| Create parallel "test workflow" | Disable blocks in real workflow to isolate paths |
| Deploy to test changes | Use useDraftState: true (the default) |
| Modify block config during tests | Only toggle enabled/disabled — never change logic |
| Guess at tool output format | Run Phase 1 JS validation to verify output shape |
| Skip block restore after tests | Always restore from snapshot, even if tests fail |
| Batch-execute multiple scenarios | Test one at a time, verify, then proceed |
- CR (Conversation Round): Build webhook payload with user message → disable non-target blocks → execute → verify agent response + routing in trace spans
- Custom Tool Isolation: Phase 1 (local JS with mocks) → Phase 2 (disable agent, enable only tool block with hardcoded inputs) → verify output schema matches agent expectations
- Media/File Handling: Include attachment URLs in payload → verify media_type condition routes correctly → test with null attachments for fallback
- Webhook Payload Validation: Start from known-good template → override fields for edge cases →
CONDITION_ONLYprofile → check condition outputs in trace spans
Simple Agent: start_trigger → agent → response
Conditional: generic_webhook → router_v2 → [path_a → agent_a, path_b → agent_b] → response
API Integration: start_trigger → api → function (parse) → agent → slack
RAG: start_trigger → knowledge (retrieve) → agent (answer) → response
Block Positioning: 200-300px horizontal spacing, 50-100px vertical for branches, triggers left → responses right.
{
"fieldName": { "type": "string", "description": "Field description" },
"nested": { "type": "object", "properties": { "id": { "type": "number" } } },
"items": { "type": "array", "description": "Array of items" }
}| Issue | Solution |
|---|---|
| "Block not found" | Call get_workflow to verify current state |
| "Invalid block type" | Check spelling in blocks/registry.ts |
| "Edge already exists" / "Circular dependency" | Cannot create duplicate or looping edges |
| Block output undefined | Verify block name matches exactly |
| Issue | Solution |
|---|---|
| Tool gives wrong output | 🔥 Load sim-workflow-testing → PATH_ISOLATION profile → check trace spans for actual vs expected |
| Can't verify custom tool logic | Phase 1 JS validation first, then Phase 2 isolated workflow. See testing skill |
| Block outputs wrong in trace | get_execution_logs({ includeTraceSpans: true, details: "full" }) — each span shows block I/O |
| Condition routes wrong | CONDITION_ONLY profile — check condition outputs for branch selection |
| Blocks left disabled | Emergency restore: toggle_block_enabled(enabled=true) for all blocks |
| Test affects production | Verify useDraftState: true. Never deploy during tests. Restore blocks after |
| GraphQL error in ikas tool | Load ikas-api skill. Common: wrong fields on SearchProductVariantType, search vs input param |
| Issue | Solution |
|---|---|
| Token expired | Re-authenticate via credential selector |
| Missing scopes / provider mismatch | Check requiredScopes and oauth.provider in tool config |
| API/transform error | Check request URL, headers, body; verify transformResponse edge cases |
| Import fails / state mismatch | Ensure complete JSON; use get_workflow before modifications |
bun run dev:full # Development (app + sockets)
bun run build # Production build
bun run test # Run tests
bun run lint # Lint codeblocks/registry.ts # All registered blocks providers/registry.ts # All LLM providers
tools/registry.ts # All registered tools lib/oauth/oauth.ts # OAuth provider configs
triggers/registry.ts # All registered triggers
import { getBlock } from '@/blocks/registry'
import { createLogger } from '@sim/logger'
import type { BlockConfig } from '@/blocks/types'
import type { ToolConfig } from '@/tools/types'
import type { ProviderConfig } from '@/providers/types'This repo is regularly rebased from the upstream sim(sim-studio/sim) repository. Local customizations may conflict with upstream improvements.
When working on code that may have upstream changes:
-
Check for upstream updates before implementing: Run
git fetch upstreamandgit log HEAD..upstream/mainto see what changed upstream. If upstream has already solved the issue or added the feature, prefer the upstream solution. -
Identify obsolete local changes: After fetching upstream, compare local changes against upstream changes for the same files/features. If both solve the same problem differently, flag this conflict.
-
Report conflicts proactively: When you detect that upstream has a better or different solution than what you're implementing or what already exists locally, tell the user:
- What upstream changed
- What local change conflicts with it
- Recommend dropping or adapting the local change
-
Drop obsolete changes when necessary: If local changes duplicate upstream solutions or implement conflicting approaches, discard them unless the user explicitly asks to keep both. Avoid merging conflicting implementations.
# Fetch and view upstream changes
git fetch upstream
git log HEAD..upstream/main --oneline
# View diff for specific file
git diff HEAD..upstream/main -- <filepath>
# Rebase onto upstream
git rebase upstream/main| Situation | Action |
|---|---|
| Upstream improved same feature | Use upstream, drop local |
| Upstream added feature you were building | Skip local implementation |
| Local and upstream both partial | Combine if possible, otherwise upstream wins |
| Local-only feature | Keep local |
| Unknown if conflict exists | Ask user, don't guess |