improvement(copilot): structured metadata context + start block deprecation (#1362)

* progress

* progress

* deploy command update

* add trigger mode modal

* fix trigger icons'

* fix corners for add trigger card

* update serialization error visual in console

* works

* improvement(copilot-context): structured context for copilot

* forgot long description

* Update metadata params

* progress

* add better workflow ux

* progress

* highlighting works

* trigger card

* default agent workflow change

* fix build error

* remove any casts

* address greptile comments

* Diff input format

* address greptile comments

* improvement: ui/ux

* improvement: changed to vertical scrolling

* fix(workflow): ensure new blocks from sidebar click/drag use getUniqueBlockName (with semantic trigger base when applicable)

* Validation + build/edit mark complete

* fix trigger dropdown

* Copilot stuff (lots of it)

* Temp update prod dns

* fix trigger check

* fix

* fix trigger mode check

* Fix yaml imports

* Fix autolayout error

* fix deployed chat

* Fix copilot input text overflow

* fix trigger mode persistence in addBlock with enableTriggerMode flag passed in

* Lint

* Fix failing tests

* Reset ishosted

* Lint

* input format for legacy starter

* Fix executor

---------

Co-authored-by: Siddharth Ganesan <siddharthganesan@gmail.com>
Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
This commit is contained in:
Vikhyath Mondreti
2025-09-22 23:24:50 -07:00
committed by GitHub
parent 68df95906f
commit b7876ca466
128 changed files with 4263 additions and 1275 deletions

View File

@@ -13,6 +13,7 @@ import { hasAdminPermission } from '@/lib/permissions/utils'
import { processStreamingBlockLogs } from '@/lib/tokenization' import { processStreamingBlockLogs } from '@/lib/tokenization'
import { getEmailDomain } from '@/lib/urls/utils' import { getEmailDomain } from '@/lib/urls/utils'
import { decryptSecret, generateRequestId } from '@/lib/utils' import { decryptSecret, generateRequestId } from '@/lib/utils'
import { TriggerUtils } from '@/lib/workflows/triggers'
import { getBlock } from '@/blocks' import { getBlock } from '@/blocks'
import { Executor } from '@/executor' import { Executor } from '@/executor'
import type { BlockLog, ExecutionResult } from '@/executor/types' import type { BlockLog, ExecutionResult } from '@/executor/types'
@@ -430,9 +431,10 @@ export async function executeWorkflowForChat(
(acc, [id, block]) => { (acc, [id, block]) => {
const blockConfig = getBlock(block.type) const blockConfig = getBlock(block.type)
const isTriggerBlock = blockConfig?.category === 'triggers' const isTriggerBlock = blockConfig?.category === 'triggers'
const isChatTrigger = block.type === 'chat_trigger'
// Skip trigger blocks during chat execution // Keep all non-trigger blocks and also keep the chat_trigger block
if (!isTriggerBlock) { if (!isTriggerBlock || isChatTrigger) {
acc[id] = block acc[id] = block
} }
return acc return acc
@@ -487,8 +489,10 @@ export async function executeWorkflowForChat(
// Filter edges to exclude connections to/from trigger blocks (same as manual execution) // Filter edges to exclude connections to/from trigger blocks (same as manual execution)
const triggerBlockIds = Object.keys(mergedStates).filter((id) => { const triggerBlockIds = Object.keys(mergedStates).filter((id) => {
const blockConfig = getBlock(mergedStates[id].type) const type = mergedStates[id].type
return blockConfig?.category === 'triggers' const blockConfig = getBlock(type)
// Exclude chat_trigger from the list so its edges are preserved
return blockConfig?.category === 'triggers' && type !== 'chat_trigger'
}) })
const filteredEdges = edges.filter( const filteredEdges = edges.filter(
@@ -613,9 +617,29 @@ export async function executeWorkflowForChat(
// Set up logging on the executor // Set up logging on the executor
loggingSession.setupExecutor(executor) loggingSession.setupExecutor(executor)
// Determine the start block for chat execution
const startBlock = TriggerUtils.findStartBlock(mergedStates, 'chat')
if (!startBlock) {
const errorMessage =
'No Chat trigger configured for this workflow. Add a Chat Trigger block to enable chat execution.'
logger.error(`[${requestId}] ${errorMessage}`)
await loggingSession.safeCompleteWithError({
endedAt: new Date().toISOString(),
totalDurationMs: 0,
error: {
message: errorMessage,
stackTrace: undefined,
},
})
throw new Error(errorMessage)
}
const startBlockId = startBlock.blockId
let result let result
try { try {
result = await executor.execute(workflowId) result = await executor.execute(workflowId, startBlockId)
} catch (error: any) { } catch (error: any) {
logger.error(`[${requestId}] Chat workflow execution failed:`, error) logger.error(`[${requestId}] Chat workflow execution failed:`, error)
await loggingSession.safeCompleteWithError({ await loggingSession.safeCompleteWithError({

View File

@@ -220,13 +220,20 @@ describe('Copilot Chat API Route', () => {
content: 'Hello', content: 'Hello',
}, },
], ],
chatMessages: [
{
role: 'user',
content: 'Hello',
},
],
workflowId: 'workflow-123', workflowId: 'workflow-123',
userId: 'user-123', userId: 'user-123',
stream: true, stream: true,
streamToolCalls: true, streamToolCalls: true,
model: 'gpt-5',
mode: 'agent', mode: 'agent',
messageId: 'mock-uuid-1234-5678', messageId: 'mock-uuid-1234-5678',
depth: 0, version: '1.0.0',
chatId: 'chat-123', chatId: 'chat-123',
}), }),
}) })
@@ -284,13 +291,19 @@ describe('Copilot Chat API Route', () => {
{ role: 'assistant', content: 'Previous response' }, { role: 'assistant', content: 'Previous response' },
{ role: 'user', content: 'New message' }, { role: 'user', content: 'New message' },
], ],
chatMessages: [
{ role: 'user', content: 'Previous message' },
{ role: 'assistant', content: 'Previous response' },
{ role: 'user', content: 'New message' },
],
workflowId: 'workflow-123', workflowId: 'workflow-123',
userId: 'user-123', userId: 'user-123',
stream: true, stream: true,
streamToolCalls: true, streamToolCalls: true,
model: 'gpt-5',
mode: 'agent', mode: 'agent',
messageId: 'mock-uuid-1234-5678', messageId: 'mock-uuid-1234-5678',
depth: 0, version: '1.0.0',
chatId: 'chat-123', chatId: 'chat-123',
}), }),
}) })
@@ -337,13 +350,18 @@ describe('Copilot Chat API Route', () => {
{ role: 'system', content: 'User seems confused about the workflow' }, { role: 'system', content: 'User seems confused about the workflow' },
{ role: 'user', content: 'Hello' }, { role: 'user', content: 'Hello' },
], ],
chatMessages: [
{ role: 'system', content: 'User seems confused about the workflow' },
{ role: 'user', content: 'Hello' },
],
workflowId: 'workflow-123', workflowId: 'workflow-123',
userId: 'user-123', userId: 'user-123',
stream: true, stream: true,
streamToolCalls: true, streamToolCalls: true,
model: 'gpt-5',
mode: 'agent', mode: 'agent',
messageId: 'mock-uuid-1234-5678', messageId: 'mock-uuid-1234-5678',
depth: 0, version: '1.0.0',
chatId: 'chat-123', chatId: 'chat-123',
}), }),
}) })
@@ -427,13 +445,15 @@ describe('Copilot Chat API Route', () => {
expect.objectContaining({ expect.objectContaining({
body: JSON.stringify({ body: JSON.stringify({
messages: [{ role: 'user', content: 'What is this workflow?' }], messages: [{ role: 'user', content: 'What is this workflow?' }],
chatMessages: [{ role: 'user', content: 'What is this workflow?' }],
workflowId: 'workflow-123', workflowId: 'workflow-123',
userId: 'user-123', userId: 'user-123',
stream: true, stream: true,
streamToolCalls: true, streamToolCalls: true,
model: 'gpt-5',
mode: 'ask', mode: 'ask',
messageId: 'mock-uuid-1234-5678', messageId: 'mock-uuid-1234-5678',
depth: 0, version: '1.0.0',
chatId: 'chat-123', chatId: 'chat-123',
}), }),
}) })

View File

@@ -15,7 +15,7 @@ import { getCopilotModel } from '@/lib/copilot/config'
import type { CopilotProviderConfig } from '@/lib/copilot/types' import type { CopilotProviderConfig } from '@/lib/copilot/types'
import { env } from '@/lib/env' import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent' import { SIM_AGENT_API_URL_DEFAULT, SIM_AGENT_VERSION } from '@/lib/sim-agent'
import { generateChatTitle } from '@/lib/sim-agent/utils' import { generateChatTitle } from '@/lib/sim-agent/utils'
import { createFileContent, isSupportedFileType } from '@/lib/uploads/file-utils' import { createFileContent, isSupportedFileType } from '@/lib/uploads/file-utils'
import { S3_COPILOT_CONFIG } from '@/lib/uploads/setup' import { S3_COPILOT_CONFIG } from '@/lib/uploads/setup'
@@ -38,8 +38,21 @@ const ChatMessageSchema = z.object({
userMessageId: z.string().optional(), // ID from frontend for the user message userMessageId: z.string().optional(), // ID from frontend for the user message
chatId: z.string().optional(), chatId: z.string().optional(),
workflowId: z.string().min(1, 'Workflow ID is required'), workflowId: z.string().min(1, 'Workflow ID is required'),
model: z
.enum([
'gpt-5-fast',
'gpt-5',
'gpt-5-medium',
'gpt-5-high',
'gpt-4o',
'gpt-4.1',
'o3',
'claude-4-sonnet',
'claude-4.1-opus',
])
.optional()
.default('gpt-5'),
mode: z.enum(['ask', 'agent']).optional().default('agent'), mode: z.enum(['ask', 'agent']).optional().default('agent'),
depth: z.number().int().min(0).max(3).optional().default(0),
prefetch: z.boolean().optional(), prefetch: z.boolean().optional(),
createNewChat: z.boolean().optional().default(false), createNewChat: z.boolean().optional().default(false),
stream: z.boolean().optional().default(true), stream: z.boolean().optional().default(true),
@@ -97,8 +110,8 @@ export async function POST(req: NextRequest) {
userMessageId, userMessageId,
chatId, chatId,
workflowId, workflowId,
model,
mode, mode,
depth,
prefetch, prefetch,
createNewChat, createNewChat,
stream, stream,
@@ -147,19 +160,6 @@ export async function POST(req: NextRequest) {
} }
} }
// Consolidation mapping: map negative depths to base depth with prefetch=true
let effectiveDepth: number | undefined = typeof depth === 'number' ? depth : undefined
let effectivePrefetch: boolean | undefined = prefetch
if (typeof effectiveDepth === 'number') {
if (effectiveDepth === -2) {
effectiveDepth = 1
effectivePrefetch = true
} else if (effectiveDepth === -1) {
effectiveDepth = 0
effectivePrefetch = true
}
}
// Handle chat context // Handle chat context
let currentChat: any = null let currentChat: any = null
let conversationHistory: any[] = [] let conversationHistory: any[] = []
@@ -366,16 +366,18 @@ export async function POST(req: NextRequest) {
const requestPayload = { const requestPayload = {
messages: messagesForAgent, messages: messagesForAgent,
chatMessages: messages, // Full unfiltered messages array
workflowId, workflowId,
userId: authenticatedUserId, userId: authenticatedUserId,
stream: stream, stream: stream,
streamToolCalls: true, streamToolCalls: true,
model: model,
mode: mode, mode: mode,
messageId: userMessageIdToUse, messageId: userMessageIdToUse,
version: SIM_AGENT_VERSION,
...(providerConfig ? { provider: providerConfig } : {}), ...(providerConfig ? { provider: providerConfig } : {}),
...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}), ...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}),
...(typeof effectiveDepth === 'number' ? { depth: effectiveDepth } : {}), ...(typeof prefetch === 'boolean' ? { prefetch: prefetch } : {}),
...(typeof effectivePrefetch === 'boolean' ? { prefetch: effectivePrefetch } : {}),
...(session?.user?.name && { userName: session.user.name }), ...(session?.user?.name && { userName: session.user.name }),
...(agentContexts.length > 0 && { context: agentContexts }), ...(agentContexts.length > 0 && { context: agentContexts }),
...(actualChatId ? { chatId: actualChatId } : {}), ...(actualChatId ? { chatId: actualChatId } : {}),
@@ -384,6 +386,9 @@ export async function POST(req: NextRequest) {
try { try {
logger.info(`[${tracker.requestId}] About to call Sim Agent with context`, { logger.info(`[${tracker.requestId}] About to call Sim Agent with context`, {
context: (requestPayload as any).context, context: (requestPayload as any).context,
messagesCount: messagesForAgent.length,
chatMessagesCount: messages.length,
hasConversationId: !!effectiveConversationId,
}) })
} catch {} } catch {}

View File

@@ -48,6 +48,7 @@ export async function POST(req: NextRequest) {
return createBadRequestResponse('Invalid request body for execute-copilot-server-tool') return createBadRequestResponse('Invalid request body for execute-copilot-server-tool')
} }
logger.error(`[${tracker.requestId}] Failed to execute server tool:`, error) logger.error(`[${tracker.requestId}] Failed to execute server tool:`, error)
return createInternalServerErrorResponse('Failed to execute server tool') const errorMessage = error instanceof Error ? error.message : 'Failed to execute server tool'
return createInternalServerErrorResponse(errorMessage)
} }
} }

View File

@@ -292,7 +292,7 @@ describe('Workflow Execution API Route', () => {
const Executor = (await import('@/executor')).Executor const Executor = (await import('@/executor')).Executor
expect(Executor).toHaveBeenCalled() expect(Executor).toHaveBeenCalled()
expect(executeMock).toHaveBeenCalledWith('workflow-id') expect(executeMock).toHaveBeenCalledWith('workflow-id', 'starter-id')
}) })
/** /**
@@ -337,7 +337,7 @@ describe('Workflow Execution API Route', () => {
const Executor = (await import('@/executor')).Executor const Executor = (await import('@/executor')).Executor
expect(Executor).toHaveBeenCalled() expect(Executor).toHaveBeenCalled()
expect(executeMock).toHaveBeenCalledWith('workflow-id') expect(executeMock).toHaveBeenCalledWith('workflow-id', 'starter-id')
expect(Executor).toHaveBeenCalledWith( expect(Executor).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({

View File

@@ -14,6 +14,7 @@ import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import { decryptSecret, generateRequestId } from '@/lib/utils' import { decryptSecret, generateRequestId } from '@/lib/utils'
import { loadDeployedWorkflowState } from '@/lib/workflows/db-helpers' import { loadDeployedWorkflowState } from '@/lib/workflows/db-helpers'
import { TriggerUtils } from '@/lib/workflows/triggers'
import { import {
createHttpResponseFromBlock, createHttpResponseFromBlock,
updateWorkflowRunCounts, updateWorkflowRunCounts,
@@ -272,6 +273,32 @@ async function executeWorkflow(
true // Enable validation during execution true // Enable validation during execution
) )
// Determine API trigger start block
// Direct API execution ONLY works with API trigger blocks (or legacy starter in api/run mode)
const startBlock = TriggerUtils.findStartBlock(mergedStates, 'api', false) // isChildWorkflow = false
if (!startBlock) {
logger.error(`[${requestId}] No API trigger configured for this workflow`)
throw new Error(
'No API trigger configured for this workflow. Add an API Trigger block or use a Start block in API mode.'
)
}
const startBlockId = startBlock.blockId
const triggerBlock = startBlock.block
// Check if the API trigger has any outgoing connections (except for legacy starter blocks)
// Legacy starter blocks have their own validation in the executor
if (triggerBlock.type !== 'starter') {
const outgoingConnections = serializedWorkflow.connections.filter(
(conn) => conn.source === startBlockId
)
if (outgoingConnections.length === 0) {
logger.error(`[${requestId}] API trigger has no outgoing connections`)
throw new Error('API Trigger block must be connected to other blocks to execute')
}
}
const executor = new Executor({ const executor = new Executor({
workflow: serializedWorkflow, workflow: serializedWorkflow,
currentBlockStates: processedBlockStates, currentBlockStates: processedBlockStates,
@@ -287,7 +314,7 @@ async function executeWorkflow(
// Set up logging on the executor // Set up logging on the executor
loggingSession.setupExecutor(executor) loggingSession.setupExecutor(executor)
const result = await executor.execute(workflowId) const result = await executor.execute(workflowId, startBlockId)
// Check if we got a StreamingExecution result (with stream + execution properties) // Check if we got a StreamingExecution result (with stream + execution properties)
// For API routes, we only care about the ExecutionResult part, not the stream // For API routes, we only care about the ExecutionResult part, not the stream

View File

@@ -12,7 +12,7 @@ import {
loadWorkflowFromNormalizedTables, loadWorkflowFromNormalizedTables,
saveWorkflowToNormalizedTables, saveWorkflowToNormalizedTables,
} from '@/lib/workflows/db-helpers' } from '@/lib/workflows/db-helpers'
import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation' import { sanitizeAgentToolsInBlocks, validateWorkflowState } from '@/lib/workflows/validation'
import { getUserId } from '@/app/api/auth/oauth/utils' import { getUserId } from '@/app/api/auth/oauth/utils'
import { getAllBlocks, getBlock } from '@/blocks' import { getAllBlocks, getBlock } from '@/blocks'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
@@ -240,6 +240,65 @@ async function upsertCustomToolsFromBlocks(
} }
} }
/**
* Convert blocks with 'inputs' field to standard 'subBlocks' structure
* This handles trigger blocks that may come from YAML/copilot with legacy format
*/
function normalizeBlockStructure(blocks: Record<string, any>): Record<string, any> {
const normalizedBlocks: Record<string, any> = {}
for (const [blockId, block] of Object.entries(blocks)) {
const normalizedBlock = { ...block }
// Normalize position coordinates (handle both uppercase and lowercase)
if (block.position) {
normalizedBlock.position = {
x: block.position.x ?? block.position.X ?? 0,
y: block.position.y ?? block.position.Y ?? 0,
}
}
// Convert any inputs map into subBlocks for consistency (applies to all blocks)
if (block.inputs) {
// Convert inputs.inputFormat to subBlocks.inputFormat
if (block.inputs.inputFormat) {
if (!normalizedBlock.subBlocks) {
normalizedBlock.subBlocks = {}
}
normalizedBlock.subBlocks.inputFormat = {
id: 'inputFormat',
type: 'input-format',
value: block.inputs.inputFormat,
}
}
// Copy all inputs fields to subBlocks (creating entries as needed)
for (const [inputKey, inputValue] of Object.entries(block.inputs)) {
if (!normalizedBlock.subBlocks) {
normalizedBlock.subBlocks = {}
}
if (!normalizedBlock.subBlocks[inputKey]) {
normalizedBlock.subBlocks[inputKey] = {
id: inputKey,
type: 'short-input', // Default type, may need adjustment based on actual field
value: inputValue,
}
} else {
normalizedBlock.subBlocks[inputKey].value = inputValue
}
}
// Remove the inputs field after conversion
normalizedBlock.inputs = undefined
}
normalizedBlocks[blockId] = normalizedBlock
}
return normalizedBlocks
}
/** /**
* PUT /api/workflows/[id]/yaml * PUT /api/workflows/[id]/yaml
* Consolidated YAML workflow saving endpoint * Consolidated YAML workflow saving endpoint
@@ -344,39 +403,76 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
}) })
} }
// Normalize blocks that use 'inputs' field to standard 'subBlocks' structure
if (workflowState.blocks) {
workflowState.blocks = normalizeBlockStructure(workflowState.blocks)
}
// Validate the workflow state before persisting
const validation = validateWorkflowState(workflowState, { sanitize: true })
if (!validation.valid) {
logger.error(`[${requestId}] Workflow validation failed`, {
errors: validation.errors,
warnings: validation.warnings,
})
return NextResponse.json({
success: false,
message: 'Invalid workflow structure',
errors: validation.errors,
warnings: validation.warnings || [],
})
}
// Use sanitized state if available
const finalWorkflowState = validation.sanitizedState || workflowState
if (validation.warnings.length > 0) {
logger.warn(`[${requestId}] Workflow validation warnings`, {
warnings: validation.warnings,
})
}
// Ensure all blocks have required fields // Ensure all blocks have required fields
Object.values(workflowState.blocks).forEach((block: any) => { Object.entries(finalWorkflowState.blocks).forEach(([blockId, block]) => {
if (block.enabled === undefined) { const blockData = block as any
block.enabled = true if (!blockData.id) blockData.id = blockId
if (!blockData.position) {
blockData.position = { x: 0, y: 0 }
} }
if (block.horizontalHandles === undefined) { if (blockData.enabled === undefined) {
block.horizontalHandles = true blockData.enabled = true
} }
if (block.isWide === undefined) { if (blockData.horizontalHandles === undefined) {
block.isWide = false blockData.horizontalHandles = true
} }
if (block.height === undefined) { if (blockData.isWide === undefined) {
block.height = 0 blockData.isWide = false
} }
if (!block.subBlocks) { if (blockData.height === undefined) {
block.subBlocks = {} blockData.height = 0
} }
if (!block.outputs) { if (!blockData.subBlocks) {
block.outputs = {} blockData.subBlocks = {}
}
if (!blockData.outputs) {
blockData.outputs = {}
} }
}) })
const blocks = Object.values(workflowState.blocks) as Array<{ const blocks = Object.values(finalWorkflowState.blocks) as Array<{
id: string id: string
type: string type: string
name: string name: string
position: { x: number; y: number } position: { x: number; y: number }
subBlocks?: Record<string, any> subBlocks?: Record<string, any>
inputs?: Record<string, any>
triggerMode?: boolean
data?: Record<string, any> data?: Record<string, any>
parentId?: string parentId?: string
extent?: string extent?: string
}> }>
const edges = workflowState.edges const edges = finalWorkflowState.edges
const warnings = conversionResult.warnings || [] const warnings = conversionResult.warnings || []
// Create workflow state // Create workflow state
@@ -454,6 +550,25 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
}) })
} }
// Handle blocks that have inputs instead of subBlocks (from YAML/copilot format)
// This is especially important for trigger configuration
if (block.inputs) {
Object.entries(block.inputs).forEach(([inputKey, inputValue]) => {
const matchingSubBlock = blockConfig.subBlocks.find((sb) => sb.id === inputKey)
if (!subBlocks[inputKey]) {
subBlocks[inputKey] = {
id: inputKey,
type:
matchingSubBlock?.type ||
(inputKey === 'triggerConfig' ? 'trigger-config' : 'short-input'),
value: inputValue,
}
} else if (inputValue !== undefined) {
subBlocks[inputKey].value = inputValue
}
})
}
// Set up outputs from block configuration // Set up outputs from block configuration
const outputs = resolveOutputType(blockConfig.outputs) const outputs = resolveOutputType(blockConfig.outputs)
@@ -476,10 +591,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
isWide: false, isWide: false,
advancedMode: false, advancedMode: false,
height: 0, height: 0,
triggerMode: block.triggerMode || false, // Preserve triggerMode from imported block
data: blockData, data: blockData,
} }
logger.debug(`[${requestId}] Processed regular block: ${block.id} -> ${newId}`) logger.debug(`[${requestId}] Processed regular block: ${block.id} -> ${newId}`, {
blockType: block.type,
hasTriggerMode: block.triggerMode,
hasInputs: !!block.inputs,
inputKeys: block.inputs ? Object.keys(block.inputs) : [],
subBlockKeys: Object.keys(subBlocks),
})
} else { } else {
logger.warn(`[${requestId}] Unknown block type: ${block.type}`) logger.warn(`[${requestId}] Unknown block type: ${block.type}`)
} }

View File

@@ -1,5 +1,5 @@
import { db } from '@sim/db' import { db } from '@sim/db'
import { workflow, workflowBlocks, workspace } from '@sim/db/schema' import { workflow, workspace } from '@sim/db/schema'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server' import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod' import { z } from 'zod'
@@ -95,132 +95,31 @@ export async function POST(req: NextRequest) {
const { name, description, color, workspaceId, folderId } = CreateWorkflowSchema.parse(body) const { name, description, color, workspaceId, folderId } = CreateWorkflowSchema.parse(body)
const workflowId = crypto.randomUUID() const workflowId = crypto.randomUUID()
const starterId = crypto.randomUUID()
const now = new Date() const now = new Date()
logger.info(`[${requestId}] Creating workflow ${workflowId} for user ${session.user.id}`) logger.info(`[${requestId}] Creating workflow ${workflowId} for user ${session.user.id}`)
await db.transaction(async (tx) => { await db.insert(workflow).values({
await tx.insert(workflow).values({ id: workflowId,
id: workflowId, userId: session.user.id,
userId: session.user.id, workspaceId: workspaceId || null,
workspaceId: workspaceId || null, folderId: folderId || null,
folderId: folderId || null, name,
name, description,
description, color,
color, lastSynced: now,
lastSynced: now, createdAt: now,
createdAt: now, updatedAt: now,
updatedAt: now, isDeployed: false,
isDeployed: false, collaborators: [],
collaborators: [], runCount: 0,
runCount: 0, variables: {},
variables: {}, isPublished: false,
isPublished: false, marketplaceData: null,
marketplaceData: null,
})
await tx.insert(workflowBlocks).values({
id: starterId,
workflowId: workflowId,
type: 'starter',
name: 'Start',
positionX: '100',
positionY: '100',
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
triggerMode: false,
height: '95',
subBlocks: {
startWorkflow: {
id: 'startWorkflow',
type: 'dropdown',
value: 'manual',
},
webhookPath: {
id: 'webhookPath',
type: 'short-input',
value: '',
},
webhookSecret: {
id: 'webhookSecret',
type: 'short-input',
value: '',
},
scheduleType: {
id: 'scheduleType',
type: 'dropdown',
value: 'daily',
},
minutesInterval: {
id: 'minutesInterval',
type: 'short-input',
value: '',
},
minutesStartingAt: {
id: 'minutesStartingAt',
type: 'short-input',
value: '',
},
hourlyMinute: {
id: 'hourlyMinute',
type: 'short-input',
value: '',
},
dailyTime: {
id: 'dailyTime',
type: 'short-input',
value: '',
},
weeklyDay: {
id: 'weeklyDay',
type: 'dropdown',
value: 'MON',
},
weeklyDayTime: {
id: 'weeklyDayTime',
type: 'short-input',
value: '',
},
monthlyDay: {
id: 'monthlyDay',
type: 'short-input',
value: '',
},
monthlyTime: {
id: 'monthlyTime',
type: 'short-input',
value: '',
},
cronExpression: {
id: 'cronExpression',
type: 'short-input',
value: '',
},
timezone: {
id: 'timezone',
type: 'dropdown',
value: 'UTC',
},
},
outputs: {
response: {
type: {
input: 'any',
},
},
},
createdAt: now,
updatedAt: now,
})
logger.info(
`[${requestId}] Successfully created workflow ${workflowId} with start block in workflow_blocks table`
)
}) })
logger.info(`[${requestId}] Successfully created empty workflow ${workflowId}`)
return NextResponse.json({ return NextResponse.json({
id: workflowId, id: workflowId,
name, name,

View File

@@ -1,5 +1,5 @@
import { db } from '@sim/db' import { db } from '@sim/db'
import { permissions, workflow, workflowBlocks, workspace } from '@sim/db/schema' import { permissions, workflow, workspace } from '@sim/db/schema'
import { and, desc, eq, isNull } from 'drizzle-orm' import { and, desc, eq, isNull } from 'drizzle-orm'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth' import { getSession } from '@/lib/auth'
@@ -110,9 +110,7 @@ async function createWorkspace(userId: string, name: string) {
updatedAt: now, updatedAt: now,
}) })
// Create initial workflow for the workspace with start block // Create initial workflow for the workspace (empty canvas)
const starterId = crypto.randomUUID()
// Create the workflow // Create the workflow
await tx.insert(workflow).values({ await tx.insert(workflow).values({
id: workflowId, id: workflowId,
@@ -133,61 +131,7 @@ async function createWorkspace(userId: string, name: string) {
marketplaceData: null, marketplaceData: null,
}) })
// Insert the start block into workflow_blocks table // No blocks are inserted - empty canvas
await tx.insert(workflowBlocks).values({
id: starterId,
workflowId: workflowId,
type: 'starter',
name: 'Start',
positionX: '100',
positionY: '100',
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: '95',
subBlocks: {
startWorkflow: {
id: 'startWorkflow',
type: 'dropdown',
value: 'manual',
},
webhookPath: {
id: 'webhookPath',
type: 'short-input',
value: '',
},
webhookSecret: {
id: 'webhookSecret',
type: 'short-input',
value: '',
},
scheduleType: {
id: 'scheduleType',
type: 'dropdown',
value: 'daily',
},
minutesInterval: {
id: 'minutesInterval',
type: 'short-input',
value: '',
},
minutesStartingAt: {
id: 'minutesStartingAt',
type: 'short-input',
value: '',
},
},
outputs: {
response: {
type: {
input: 'any',
},
},
},
createdAt: now,
updatedAt: now,
})
logger.info( logger.info(
`Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}` `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}`

View File

@@ -4,6 +4,7 @@ import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent' import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils' import { generateRequestId } from '@/lib/utils'
import { validateWorkflowState } from '@/lib/workflows/validation'
import { getAllBlocks } from '@/blocks/registry' import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { resolveOutputType } from '@/blocks/utils' import { resolveOutputType } from '@/blocks/utils'
@@ -61,6 +62,59 @@ const CreateDiffRequestSchema = z.object({
.optional(), .optional(),
}) })
/**
* Convert blocks with 'inputs' field to standard 'subBlocks' structure
* This handles trigger blocks that may come from YAML/copilot with legacy format
*/
function normalizeBlockStructure(blocks: Record<string, any>): Record<string, any> {
const normalizedBlocks: Record<string, any> = {}
for (const [blockId, block] of Object.entries(blocks)) {
const normalizedBlock = { ...block }
// Check if this is a trigger block with 'inputs' field
if (
block.inputs &&
(block.type === 'api_trigger' ||
block.type === 'input_trigger' ||
block.type === 'starter' ||
block.type === 'chat_trigger' ||
block.type === 'generic_webhook')
) {
// Convert inputs.inputFormat to subBlocks.inputFormat
if (block.inputs.inputFormat) {
if (!normalizedBlock.subBlocks) {
normalizedBlock.subBlocks = {}
}
normalizedBlock.subBlocks.inputFormat = {
id: 'inputFormat',
type: 'input-format',
value: block.inputs.inputFormat,
}
}
// Copy any other inputs fields to subBlocks
for (const [inputKey, inputValue] of Object.entries(block.inputs)) {
if (inputKey !== 'inputFormat' && !normalizedBlock.subBlocks[inputKey]) {
normalizedBlock.subBlocks[inputKey] = {
id: inputKey,
type: 'short-input', // Default type, may need adjustment based on actual field
value: inputValue,
}
}
}
// Remove the inputs field after conversion
normalizedBlock.inputs = undefined
}
normalizedBlocks[blockId] = normalizedBlock
}
return normalizedBlocks
}
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const requestId = generateRequestId() const requestId = generateRequestId()
@@ -202,6 +256,46 @@ export async function POST(request: NextRequest) {
const finalResult = result const finalResult = result
if (result.success && result.diff?.proposedState) { if (result.success && result.diff?.proposedState) {
// Normalize blocks that use 'inputs' field to standard 'subBlocks' structure
if (result.diff.proposedState.blocks) {
result.diff.proposedState.blocks = normalizeBlockStructure(result.diff.proposedState.blocks)
}
// Validate the proposed workflow state
const validation = validateWorkflowState(result.diff.proposedState, { sanitize: true })
if (!validation.valid) {
logger.error(`[${requestId}] Proposed workflow state validation failed`, {
errors: validation.errors,
warnings: validation.warnings,
})
return NextResponse.json(
{
success: false,
errors: validation.errors,
},
{ status: 400 }
)
}
// Use sanitized state if available
if (validation.sanitizedState) {
result.diff.proposedState = validation.sanitizedState
}
if (validation.warnings.length > 0) {
logger.warn(`[${requestId}] Proposed workflow validation warnings`, {
warnings: validation.warnings,
})
// Include warnings in the response
if (!result.warnings) {
result.warnings = []
}
result.warnings.push(...validation.warnings)
}
logger.info(`[${requestId}] Successfully created diff with normalized and validated blocks`)
// First, fix parent-child relationships based on edges // First, fix parent-child relationships based on edges
const blocks = result.diff.proposedState.blocks const blocks = result.diff.proposedState.blocks
const edges = result.diff.proposedState.edges || [] const edges = result.diff.proposedState.edges || []
@@ -271,6 +365,9 @@ export async function POST(request: NextRequest) {
if (result.success && result.blocks && !result.diff) { if (result.success && result.blocks && !result.diff) {
logger.info(`[${requestId}] Transforming sim agent blocks response to diff format`) logger.info(`[${requestId}] Transforming sim agent blocks response to diff format`)
// Normalize blocks that use 'inputs' field to standard 'subBlocks' structure
result.blocks = normalizeBlockStructure(result.blocks)
// First, fix parent-child relationships based on edges // First, fix parent-child relationships based on edges
const blocks = result.blocks const blocks = result.blocks
const edges = result.edges || [] const edges = result.edges || []

View File

@@ -85,10 +85,16 @@ export function DeployModal({
let inputFormatExample = '' let inputFormatExample = ''
try { try {
const blocks = Object.values(useWorkflowStore.getState().blocks) const blocks = Object.values(useWorkflowStore.getState().blocks)
// Check for API trigger block first (takes precedence)
const apiTriggerBlock = blocks.find((block) => block.type === 'api_trigger')
// Fall back to legacy starter block
const starterBlock = blocks.find((block) => block.type === 'starter') const starterBlock = blocks.find((block) => block.type === 'starter')
if (starterBlock) { const targetBlock = apiTriggerBlock || starterBlock
const inputFormat = useSubBlockStore.getState().getValue(starterBlock.id, 'inputFormat')
if (targetBlock) {
const inputFormat = useSubBlockStore.getState().getValue(targetBlock.id, 'inputFormat')
if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) { if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) {
const exampleData: Record<string, any> = {} const exampleData: Record<string, any> = {}

View File

@@ -330,6 +330,25 @@ export function Chat({ chatMessage, setChatMessage }: ChatProps) {
if (event === 'final' && data) { if (event === 'final' && data) {
const result = data as ExecutionResult const result = data as ExecutionResult
// If final result is a failure, surface error and stop
if ('success' in result && !result.success) {
addMessage({
content: `Error: ${result.error || 'Workflow execution failed'}`,
workflowId: activeWorkflowId,
type: 'workflow',
})
// Clear any existing message streams
for (const msgId of messageIdMap.values()) {
finalizeMessageStream(msgId)
}
messageIdMap.clear()
// Stop processing
return
}
const nonStreamingLogs = const nonStreamingLogs =
result.logs?.filter((log) => !messageIdMap.has(log.blockId)) || [] result.logs?.filter((log) => !messageIdMap.has(log.blockId)) || []
@@ -343,34 +362,25 @@ export function Chat({ chatMessage, setChatMessage }: ChatProps) {
const blockIdForOutput = extractBlockIdFromOutputId(outputId) const blockIdForOutput = extractBlockIdFromOutputId(outputId)
const path = extractPathFromOutputId(outputId, blockIdForOutput) const path = extractPathFromOutputId(outputId, blockIdForOutput)
const log = nonStreamingLogs.find((l) => l.blockId === blockIdForOutput) const log = nonStreamingLogs.find((l) => l.blockId === blockIdForOutput)
if (log) { if (log) {
let outputValue: any = log.output let output = log.output
if (path) { if (path) {
// Parse JSON content safely output = parseOutputContentSafely(output)
outputValue = parseOutputContentSafely(outputValue)
const pathParts = path.split('.') const pathParts = path.split('.')
let current = output
for (const part of pathParts) { for (const part of pathParts) {
if ( if (current && typeof current === 'object' && part in current) {
outputValue && current = current[part]
typeof outputValue === 'object' &&
part in outputValue
) {
outputValue = outputValue[part]
} else { } else {
outputValue = undefined current = undefined
break break
} }
} }
output = current
} }
if (outputValue !== undefined) { if (output !== undefined) {
addMessage({ addMessage({
content: content: typeof output === 'string' ? output : JSON.stringify(output),
typeof outputValue === 'string'
? outputValue
: `\`\`\`json\n${JSON.stringify(outputValue, null, 2)}\n\`\`\``,
workflowId: activeWorkflowId, workflowId: activeWorkflowId,
type: 'workflow', type: 'workflow',
}) })

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { format } from 'date-fns' import { format } from 'date-fns'
import { import {
AlertCircle, AlertCircle,
AlertTriangle,
Check, Check,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
@@ -369,8 +370,10 @@ export function ConsoleEntry({ entry, consoleWidth }: ConsoleEntryProps) {
} }
}, [showCopySuccess]) }, [showCopySuccess])
const BlockIcon = blockConfig?.icon // Special handling for serialization errors
const blockColor = blockConfig?.bgColor || '#6B7280' const BlockIcon = entry.blockType === 'serializer' ? AlertTriangle : blockConfig?.icon
const blockColor =
entry.blockType === 'serializer' ? '#EF4444' : blockConfig?.bgColor || '#6B7280'
// Handle image load error callback // Handle image load error callback
const handleImageLoadError = (hasError: boolean) => { const handleImageLoadError = (hasError: boolean) => {

View File

@@ -456,46 +456,6 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
) : null} ) : null}
<div className='flex items-center justify-end gap-0'> <div className='flex items-center justify-end gap-0'>
{hasCheckpoints && (
<div className='mr-1 inline-flex items-center justify-center'>
{showRestoreConfirmation ? (
<div className='inline-flex items-center gap-1'>
<button
onClick={handleConfirmRevert}
disabled={isRevertingCheckpoint}
className='text-muted-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
title='Confirm restore'
aria-label='Confirm restore'
>
{isRevertingCheckpoint ? (
<Loader2 className='h-3 w-3 animate-spin' />
) : (
<Check className='h-3 w-3' />
)}
</button>
<button
onClick={handleCancelRevert}
disabled={isRevertingCheckpoint}
className='text-muted-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
title='Cancel restore'
aria-label='Cancel restore'
>
<X className='h-3 w-3' />
</button>
</div>
) : (
<button
onClick={handleRevertToCheckpoint}
disabled={isRevertingCheckpoint}
className='text-muted-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
title='Restore workflow to this checkpoint state'
aria-label='Restore'
>
<RotateCcw className='h-3 w-3' />
</button>
)}
</div>
)}
<div className='min-w-0 max-w-[80%]'> <div className='min-w-0 max-w-[80%]'>
{/* Message content in purple box */} {/* Message content in purple box */}
<div <div
@@ -544,6 +504,48 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
})()} })()}
</div> </div>
</div> </div>
{hasCheckpoints && (
<div className='mt-1 flex h-6 items-center justify-end'>
{showRestoreConfirmation ? (
<div className='inline-flex items-center gap-1 rounded px-1 py-0.5 text-[11px] text-muted-foreground'>
<span>Restore Checkpoint?</span>
<button
onClick={handleConfirmRevert}
disabled={isRevertingCheckpoint}
className='transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
title='Confirm restore'
aria-label='Confirm restore'
>
{isRevertingCheckpoint ? (
<Loader2 className='h-3 w-3 animate-spin' />
) : (
<Check className='h-3 w-3' />
)}
</button>
<button
onClick={handleCancelRevert}
disabled={isRevertingCheckpoint}
className='transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
title='Cancel restore'
aria-label='Cancel restore'
>
<X className='h-3 w-3' />
</button>
</div>
) : (
<button
onClick={handleRevertToCheckpoint}
disabled={isRevertingCheckpoint}
className='inline-flex items-center gap-1 text-muted-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50'
title='Restore workflow to this checkpoint state'
aria-label='Restore'
>
<span className='text-[11px]'>Restore</span>
<RotateCcw className='h-3 w-3' />
</button>
)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -51,7 +51,6 @@ import {
import { useSession } from '@/lib/auth-client' import { useSession } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { CopilotSlider } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/copilot-slider'
import { useCopilotStore } from '@/stores/copilot/store' import { useCopilotStore } from '@/stores/copilot/store'
import type { ChatContext } from '@/stores/copilot/types' import type { ChatContext } from '@/stores/copilot/types'
import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -122,6 +121,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
const [dragCounter, setDragCounter] = useState(0) const [dragCounter, setDragCounter] = useState(0)
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const overlayRef = useRef<HTMLDivElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [showMentionMenu, setShowMentionMenu] = useState(false) const [showMentionMenu, setShowMentionMenu] = useState(false)
const mentionMenuRef = useRef<HTMLDivElement>(null) const mentionMenuRef = useRef<HTMLDivElement>(null)
@@ -319,15 +319,37 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
// Auto-resize textarea and toggle vertical scroll when exceeding max height // Auto-resize textarea and toggle vertical scroll when exceeding max height
useEffect(() => { useEffect(() => {
const textarea = textareaRef.current const textarea = textareaRef.current
const overlay = overlayRef.current
if (textarea) { if (textarea) {
const maxHeight = 120 const maxHeight = 120
textarea.style.height = 'auto' textarea.style.height = 'auto'
const nextHeight = Math.min(textarea.scrollHeight, maxHeight) const nextHeight = Math.min(textarea.scrollHeight, maxHeight)
textarea.style.height = `${nextHeight}px` textarea.style.height = `${nextHeight}px`
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden' textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'
// Also update overlay height to match
if (overlay) {
overlay.style.height = `${nextHeight}px`
overlay.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'
}
} }
}, [message]) }, [message])
// Sync scroll position between textarea and overlay
useEffect(() => {
const textarea = textareaRef.current
const overlay = overlayRef.current
if (!textarea || !overlay) return
const handleScroll = () => {
overlay.scrollTop = textarea.scrollTop
}
textarea.addEventListener('scroll', handleScroll)
return () => textarea.removeEventListener('scroll', handleScroll)
}, [])
// Close mention menu on outside click // Close mention menu on outside click
useEffect(() => { useEffect(() => {
if (!showMentionMenu) return if (!showMentionMenu) return
@@ -1754,55 +1776,44 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
return 'Agent' return 'Agent'
} }
// Depth toggle state comes from global store; access via useCopilotStore // Model selection state comes from global store; access via useCopilotStore
const { agentDepth, agentPrefetch, setAgentDepth, setAgentPrefetch } = useCopilotStore() const { selectedModel, agentPrefetch, setSelectedModel, setAgentPrefetch } = useCopilotStore()
// Ensure MAX mode is off for Fast and Balanced depths // Model configurations with their display names
useEffect(() => { const modelOptions = [
if (agentDepth < 2 && !agentPrefetch) { { value: 'gpt-5-fast', label: 'gpt-5-fast' },
setAgentPrefetch(true) { value: 'gpt-5', label: 'gpt-5' },
} { value: 'gpt-5-medium', label: 'gpt-5-medium' },
}, [agentDepth, agentPrefetch, setAgentPrefetch]) { value: 'gpt-5-high', label: 'gpt-5-high' },
{ value: 'gpt-4o', label: 'gpt-4o' },
const cycleDepth = () => { { value: 'gpt-4.1', label: 'gpt-4.1' },
// 8 modes: depths 0-3, each with prefetch off/on. Cycle depth, then toggle prefetch when wrapping. { value: 'o3', label: 'o3' },
const nextDepth = agentDepth === 3 ? 0 : ((agentDepth + 1) as 0 | 1 | 2 | 3) { value: 'claude-4-sonnet', label: 'claude-4-sonnet' },
if (nextDepth === 0 && agentDepth === 3) { { value: 'claude-4.1-opus', label: 'claude-4.1-opus' },
setAgentPrefetch(!agentPrefetch) ] as const
}
setAgentDepth(nextDepth)
}
const getCollapsedModeLabel = () => { const getCollapsedModeLabel = () => {
const base = getDepthLabelFor(agentDepth) const model = modelOptions.find((m) => m.value === selectedModel)
return !agentPrefetch ? `${base} MAX` : base return model ? model.label : 'GPT-5 Default'
} }
const getDepthLabelFor = (value: 0 | 1 | 2 | 3) => { const getModelIcon = () => {
return value === 0 ? 'Fast' : value === 1 ? 'Balanced' : value === 2 ? 'Advanced' : 'Behemoth'
}
// Removed descriptive suffixes; concise labels only
const getDepthDescription = (value: 0 | 1 | 2 | 3) => {
if (value === 0)
return 'Fastest and cheapest. Good for small edits, simple workflows, and small tasks'
if (value === 1) return 'Balances speed and reasoning. Good fit for most tasks'
if (value === 2)
return 'More reasoning for larger workflows and complex edits, still balanced for speed'
return 'Maximum reasoning power. Best for complex workflow building and debugging'
}
const getDepthIconFor = (value: 0 | 1 | 2 | 3) => {
const colorClass = !agentPrefetch const colorClass = !agentPrefetch
? 'text-[var(--brand-primary-hover-hex)]' ? 'text-[var(--brand-primary-hover-hex)]'
: 'text-muted-foreground' : 'text-muted-foreground'
if (value === 0) return <Zap className={`h-3 w-3 ${colorClass}`} />
if (value === 1) return <InfinityIcon className={`h-3 w-3 ${colorClass}`} />
if (value === 2) return <Brain className={`h-3 w-3 ${colorClass}`} />
return <BrainCircuit className={`h-3 w-3 ${colorClass}`} />
}
const getDepthIcon = () => getDepthIconFor(agentDepth) // Match the dropdown icon logic exactly
if (['gpt-5-high', 'o3', 'claude-4.1-opus'].includes(selectedModel)) {
return <BrainCircuit className={`h-3 w-3 ${colorClass}`} />
}
if (['gpt-5', 'gpt-5-medium', 'claude-4-sonnet'].includes(selectedModel)) {
return <Brain className={`h-3 w-3 ${colorClass}`} />
}
if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)) {
return <Zap className={`h-3 w-3 ${colorClass}`} />
}
return <InfinityIcon className={`h-3 w-3 ${colorClass}`} />
}
const scrollActiveItemIntoView = (index: number) => { const scrollActiveItemIntoView = (index: number) => {
const container = menuListRef.current const container = menuListRef.current
@@ -2116,8 +2127,11 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
{/* Textarea Field with overlay */} {/* Textarea Field with overlay */}
<div className='relative'> <div className='relative'>
{/* Highlight overlay */} {/* Highlight overlay */}
<div className='pointer-events-none absolute inset-0 z-[1] px-[2px] py-1'> <div
<pre className='whitespace-pre-wrap font-sans text-foreground text-sm leading-[1.25rem]'> ref={overlayRef}
className='pointer-events-none absolute inset-0 z-[1] max-h-[120px] overflow-y-auto overflow-x-hidden px-[2px] py-1 [&::-webkit-scrollbar]:hidden'
>
<pre className='whitespace-pre-wrap break-words font-sans text-foreground text-sm leading-[1.25rem]'>
{(() => { {(() => {
const elements: React.ReactNode[] = [] const elements: React.ReactNode[] = []
const remaining = message const remaining = message
@@ -2163,8 +2177,8 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
placeholder={isDragging ? 'Drop files here...' : effectivePlaceholder} placeholder={isDragging ? 'Drop files here...' : effectivePlaceholder}
disabled={disabled} disabled={disabled}
rows={1} rows={1}
className='relative z-[2] mb-2 min-h-[32px] w-full resize-none overflow-y-auto overflow-x-hidden border-0 bg-transparent px-[2px] py-1 font-sans text-sm text-transparent leading-[1.25rem] caret-foreground focus-visible:ring-0 focus-visible:ring-offset-0' className='relative z-[2] mb-2 min-h-[32px] w-full resize-none overflow-y-auto overflow-x-hidden break-words border-0 bg-transparent px-[2px] py-1 font-sans text-sm text-transparent leading-[1.25rem] caret-foreground focus-visible:ring-0 focus-visible:ring-offset-0'
style={{ height: 'auto' }} style={{ height: 'auto', wordBreak: 'break-word' }}
/> />
{showMentionMenu && ( {showMentionMenu && (
@@ -3057,7 +3071,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
<span>{getModeText()}</span> <span>{getModeText()}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align='start' className='p-0'> <DropdownMenuContent align='start' side='top' className='p-0'>
<TooltipProvider> <TooltipProvider>
<div className='w-[160px] p-1'> <div className='w-[160px] p-1'>
<Tooltip> <Tooltip>
@@ -3131,85 +3145,166 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
)} )}
title='Choose mode' title='Choose mode'
> >
{getDepthIcon()} {getModelIcon()}
<span>{getCollapsedModeLabel()}</span> <span>
{getCollapsedModeLabel()}
{!agentPrefetch &&
!['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel) && (
<span className='ml-1 font-semibold'>MAX</span>
)}
</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align='start' className='p-0'> <DropdownMenuContent align='start' side='top' className='max-h-[400px] p-0'>
<TooltipProvider delayDuration={100} skipDelayDuration={0}> <TooltipProvider delayDuration={100} skipDelayDuration={0}>
<div className='w-[260px] p-3'> <div className='w-[220px]'>
<div className='mb-3 flex items-center justify-between'> <div className='p-2 pb-0'>
<div className='flex items-center gap-1.5'>
<span className='font-medium text-xs'>MAX mode</span>
<Tooltip>
<TooltipTrigger asChild>
<button
type='button'
className='h-3.5 w-3.5 rounded text-muted-foreground transition-colors hover:text-foreground'
aria-label='MAX mode info'
>
<Info className='h-3.5 w-3.5' />
</button>
</TooltipTrigger>
<TooltipContent
side='right'
sideOffset={6}
align='center'
className='max-w-[220px] border bg-popover p-2 text-[11px] text-popover-foreground leading-snug shadow-md'
>
Significantly increases depth of reasoning
<br />
<span className='text-[10px] text-muted-foreground italic'>
Only available in Advanced and Behemoth modes
</span>
</TooltipContent>
</Tooltip>
</div>
<Switch
checked={!agentPrefetch}
disabled={agentDepth < 2}
title={
agentDepth < 2
? 'MAX mode is only available for Advanced or Expert'
: undefined
}
onCheckedChange={(checked) => {
if (agentDepth < 2) return
setAgentPrefetch(!checked)
}}
/>
</div>
<div className='my-2 flex justify-center'>
<div className='h-px w-[100%] bg-border' />
</div>
<div className='mb-3'>
<div className='mb-2 flex items-center justify-between'> <div className='mb-2 flex items-center justify-between'>
<span className='font-medium text-xs'>Mode</span> <div className='flex items-center gap-1.5'>
<div className='flex items-center gap-1'> <span className='font-medium text-xs'>MAX mode</span>
{getDepthIconFor(agentDepth)} <Tooltip>
<span className='text-muted-foreground text-xs'> <TooltipTrigger asChild>
{getDepthLabelFor(agentDepth)} <button
</span> type='button'
className='h-3.5 w-3.5 rounded text-muted-foreground transition-colors hover:text-foreground'
aria-label='MAX mode info'
>
<Info className='h-3.5 w-3.5' />
</button>
</TooltipTrigger>
<TooltipContent
side='right'
sideOffset={6}
align='center'
className='max-w-[220px] border bg-popover p-2 text-[11px] text-popover-foreground leading-snug shadow-md'
>
Significantly increases depth of reasoning
<br />
<span className='text-[10px] text-muted-foreground italic'>
Only available for advanced models
</span>
</TooltipContent>
</Tooltip>
</div> </div>
</div> <Switch
<div className='relative'> checked={!agentPrefetch}
<CopilotSlider disabled={['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)}
min={0} title={
max={3} ['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)
step={1} ? 'MAX mode is only available for advanced models'
value={[agentDepth]} : undefined
onValueChange={(val) =>
setAgentDepth((val?.[0] ?? 0) as 0 | 1 | 2 | 3)
} }
onCheckedChange={(checked) => {
if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel))
return
setAgentPrefetch(!checked)
}}
/> />
<div className='pointer-events-none absolute inset-0'> </div>
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-[33.333%] h-2 w-[3px] bg-background' /> <div className='my-1.5 flex justify-center'>
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-[66.667%] h-2 w-[3px] bg-background' /> <div className='h-px w-[100%] bg-border' />
</div>
</div> </div>
</div> </div>
<div className='mt-3 text-[11px] text-muted-foreground'> <div className='max-h-[280px] overflow-y-auto px-2 pb-2'>
{getDepthDescription(agentDepth)} <div>
<div className='mb-1'>
<span className='font-medium text-xs'>Model</span>
</div>
<div className='space-y-2'>
{/* Helper function to get icon for a model */}
{(() => {
const getModelIcon = (modelValue: string) => {
if (
['gpt-5-high', 'o3', 'claude-4.1-opus'].includes(modelValue)
) {
return (
<BrainCircuit className='h-3 w-3 text-muted-foreground' />
)
}
if (
['gpt-5', 'gpt-5-medium', 'claude-4-sonnet'].includes(
modelValue
)
) {
return <Brain className='h-3 w-3 text-muted-foreground' />
}
if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(modelValue)) {
return <Zap className='h-3 w-3 text-muted-foreground' />
}
return <div className='h-3 w-3' />
}
const renderModelOption = (
option: (typeof modelOptions)[number]
) => (
<DropdownMenuItem
key={option.value}
onSelect={() => {
setSelectedModel(option.value)
// Automatically turn off max mode for fast models (Zap icon)
if (
['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(
option.value
) &&
!agentPrefetch
) {
setAgentPrefetch(true)
}
}}
className={cn(
'flex h-7 items-center gap-1.5 px-2 py-1 text-left text-xs',
selectedModel === option.value ? 'bg-muted/50' : ''
)}
>
{getModelIcon(option.value)}
<span>{option.label}</span>
</DropdownMenuItem>
)
return (
<>
{/* OpenAI Models */}
<div>
<div className='px-2 py-1 font-medium text-[10px] text-muted-foreground uppercase'>
OpenAI
</div>
<div className='space-y-0.5'>
{modelOptions
.filter((option) =>
[
'gpt-5-fast',
'gpt-5',
'gpt-5-medium',
'gpt-5-high',
'gpt-4o',
'gpt-4.1',
'o3',
].includes(option.value)
)
.map(renderModelOption)}
</div>
</div>
{/* Anthropic Models */}
<div>
<div className='px-2 py-1 font-medium text-[10px] text-muted-foreground uppercase'>
Anthropic
</div>
<div className='space-y-0.5'>
{modelOptions
.filter((option) =>
['claude-4-sonnet', 'claude-4.1-opus'].includes(
option.value
)
)
.map(renderModelOption)}
</div>
</div>
</>
)
})()}
</div>
</div>
</div> </div>
</div> </div>
</TooltipProvider> </TooltipProvider>

View File

@@ -0,0 +1,249 @@
'use client'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Info, Plus, Search, X } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { getAllTriggerBlocks, getTriggerDisplayName } from '@/lib/workflows/trigger-utils'
const logger = createLogger('TriggerList')
interface TriggerListProps {
onSelect: (triggerId: string, enableTriggerMode?: boolean) => void
className?: string
}
export function TriggerList({ onSelect, className }: TriggerListProps) {
const [searchQuery, setSearchQuery] = useState('')
const [showList, setShowList] = useState(false)
const listRef = useRef<HTMLDivElement>(null)
// Get all trigger options from the centralized source
const triggerOptions = useMemo(() => getAllTriggerBlocks(), [])
// Handle escape key
useEffect(() => {
if (!showList) return
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
logger.info('Closing trigger list via escape key')
setShowList(false)
setSearchQuery('')
}
}
document.addEventListener('keydown', handleEscape)
return () => {
document.removeEventListener('keydown', handleEscape)
}
}, [showList])
const filteredOptions = useMemo(() => {
if (!searchQuery.trim()) return triggerOptions
const query = searchQuery.toLowerCase()
return triggerOptions.filter(
(option) =>
option.name.toLowerCase().includes(query) ||
option.description.toLowerCase().includes(query)
)
}, [searchQuery, triggerOptions])
const coreOptions = useMemo(
() => filteredOptions.filter((opt) => opt.category === 'core'),
[filteredOptions]
)
const integrationOptions = useMemo(
() => filteredOptions.filter((opt) => opt.category === 'integration'),
[filteredOptions]
)
const handleTriggerClick = (triggerId: string, enableTriggerMode?: boolean) => {
logger.info('Trigger selected', { triggerId, enableTriggerMode })
onSelect(triggerId, enableTriggerMode)
// Reset state after selection
setShowList(false)
setSearchQuery('')
}
const handleClose = () => {
logger.info('Closing trigger list via X button')
setShowList(false)
setSearchQuery('')
}
const TriggerItem = ({ trigger }: { trigger: (typeof triggerOptions)[0] }) => {
const Icon = trigger.icon
return (
<div
className={cn(
'flex h-10 w-[200px] flex-shrink-0 cursor-pointer items-center gap-[10px] rounded-[8px] border px-1.5 transition-all duration-200',
'border-border/40 bg-background/60 hover:border-border hover:bg-secondary/80'
)}
>
<div
onClick={() => handleTriggerClick(trigger.id, trigger.enableTriggerMode)}
className='flex flex-1 items-center gap-[10px]'
>
<div
className='relative flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-[6px]'
style={{ backgroundColor: trigger.color }}
>
{Icon ? (
<Icon className='!h-4 !w-4 text-white' />
) : (
<div className='h-4 w-4 rounded bg-white/20' />
)}
</div>
<span className='flex-1 truncate font-medium text-sm leading-none'>
{getTriggerDisplayName(trigger.id)}
</span>
</div>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<button
onClick={(e) => e.stopPropagation()}
className='flex h-6 w-6 items-center justify-center rounded-md'
>
<Info className='h-3.5 w-3.5 text-muted-foreground' />
</button>
</TooltipTrigger>
<TooltipContent
side='top'
sideOffset={5}
className='z-[9999] max-w-[200px]'
align='center'
avoidCollisions={false}
>
<p className='text-xs'>{trigger.description}</p>
</TooltipContent>
</Tooltip>
</div>
)
}
return (
<div
className={cn(
'pointer-events-none absolute inset-0 flex items-center justify-center',
className
)}
>
{!showList ? (
/* Initial Button State */
<button
onClick={() => {
logger.info('Opening trigger list')
setShowList(true)
}}
className={cn(
'pointer-events-auto',
'flex items-center gap-2',
'px-4 py-2',
'rounded-lg border border-muted-foreground/50 border-dashed',
'bg-background/95 backdrop-blur-sm',
'hover:border-muted-foreground hover:bg-muted',
'transition-all duration-200',
'font-medium text-muted-foreground text-sm'
)}
>
<Plus className='h-4 w-4' />
Click to Add Trigger
</button>
) : (
/* Trigger List View */
<div
ref={listRef}
className={cn(
'pointer-events-auto',
'max-h-[400px] w-[650px]',
'rounded-xl border border-border',
'bg-background/95 backdrop-blur-sm',
'shadow-lg',
'flex flex-col',
'relative'
)}
>
{/* Search - matching search modal exactly */}
<div className='flex items-center border-b px-4 py-1'>
<Search className='h-4 w-4 font-sans text-muted-foreground text-xl' />
<Input
placeholder='Search triggers'
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className='!font-[350] border-0 bg-transparent font-sans text-muted-foreground leading-10 tracking-normal placeholder:text-muted-foreground focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
autoFocus
/>
</div>
{/* Close button */}
<button
onClick={handleClose}
className='absolute top-4 right-4 h-4 w-4 p-0 text-muted-foreground transition-colors hover:bg-transparent hover:text-foreground focus:outline-none disabled:pointer-events-none'
tabIndex={-1}
>
<X className='h-4 w-4' />
<span className='sr-only'>Close</span>
</button>
{/* Trigger List */}
<div
className='flex-1 overflow-y-auto'
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
<div className='space-y-4 pt-4 pb-4'>
{/* Core Triggers Section */}
{coreOptions.length > 0 && (
<div>
<h3 className='mb-2 ml-4 font-normal font-sans text-[13px] text-muted-foreground leading-none tracking-normal'>
Core Triggers
</h3>
<div className='px-4 pb-1'>
{/* Display triggers in a 3-column grid */}
<div className='grid grid-cols-3 gap-2'>
{coreOptions.map((trigger) => (
<TriggerItem key={trigger.id} trigger={trigger} />
))}
</div>
</div>
</div>
)}
{/* Integration Triggers Section */}
{integrationOptions.length > 0 && (
<div>
<h3 className='mb-2 ml-4 font-normal font-sans text-[13px] text-muted-foreground leading-none tracking-normal'>
Integration Triggers
</h3>
<div
className='max-h-[200px] overflow-y-auto px-4 pb-1'
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{/* Display triggers in a 3-column grid */}
<div className='grid grid-cols-3 gap-2'>
{integrationOptions.map((trigger) => (
<TriggerItem key={trigger.id} trigger={trigger} />
))}
</div>
</div>
</div>
)}
{filteredOptions.length === 0 && (
<div className='ml-6 py-12 text-center'>
<p className='text-muted-foreground'>No results found for "{searchQuery}"</p>
</div>
)}
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,60 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
export enum TriggerWarningType {
DUPLICATE_TRIGGER = 'duplicate_trigger',
LEGACY_INCOMPATIBILITY = 'legacy_incompatibility',
}
interface TriggerWarningDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
triggerName: string
type: TriggerWarningType
}
export function TriggerWarningDialog({
open,
onOpenChange,
triggerName,
type,
}: TriggerWarningDialogProps) {
const getTitle = () => {
switch (type) {
case TriggerWarningType.LEGACY_INCOMPATIBILITY:
return 'Cannot mix trigger types'
case TriggerWarningType.DUPLICATE_TRIGGER:
return `Only one ${triggerName} trigger allowed`
}
}
const getDescription = () => {
switch (type) {
case TriggerWarningType.LEGACY_INCOMPATIBILITY:
return 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.'
case TriggerWarningType.DUPLICATE_TRIGGER:
return `A workflow can only have one ${triggerName} trigger block. Please remove the existing one before adding a new one.`
}
}
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{getTitle()}</AlertDialogTitle>
<AlertDialogDescription>{getDescription()}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction onClick={() => onOpenChange(false)}>Got it</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@@ -10,6 +10,7 @@ export { EvalInput } from './eval-input'
export { FileSelectorInput } from './file-selector/file-selector-input' export { FileSelectorInput } from './file-selector/file-selector-input'
export { FileUpload } from './file-upload' export { FileUpload } from './file-upload'
export { FolderSelectorInput } from './folder-selector/components/folder-selector-input' export { FolderSelectorInput } from './folder-selector/components/folder-selector-input'
export { InputMapping } from './input-mapping/input-mapping'
export { KnowledgeBaseSelector } from './knowledge-base-selector/knowledge-base-selector' export { KnowledgeBaseSelector } from './knowledge-base-selector/knowledge-base-selector'
export { LongInput } from './long-input' export { LongInput } from './long-input'
export { McpDynamicArgs } from './mcp-dynamic-args/mcp-dynamic-args' export { McpDynamicArgs } from './mcp-dynamic-args/mcp-dynamic-args'

View File

@@ -0,0 +1,339 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { formatDisplayText } from '@/components/ui/formatted-text'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
interface InputFormatField {
name: string
type?: string
}
interface InputTriggerBlock {
type: 'input_trigger'
subBlocks?: {
inputFormat?: { value?: InputFormatField[] }
}
}
interface StarterBlockLegacy {
type: 'starter'
subBlocks?: {
inputFormat?: { value?: InputFormatField[] }
}
config?: {
params?: {
inputFormat?: InputFormatField[]
}
}
}
function isInputTriggerBlock(value: unknown): value is InputTriggerBlock {
return (
!!value && typeof value === 'object' && (value as { type?: unknown }).type === 'input_trigger'
)
}
function isStarterBlock(value: unknown): value is StarterBlockLegacy {
return !!value && typeof value === 'object' && (value as { type?: unknown }).type === 'starter'
}
function isInputFormatField(value: unknown): value is InputFormatField {
if (typeof value !== 'object' || value === null) return false
if (!('name' in value)) return false
const { name, type } = value as { name: unknown; type?: unknown }
if (typeof name !== 'string' || name.trim() === '') return false
if (type !== undefined && typeof type !== 'string') return false
return true
}
interface InputMappingProps {
blockId: string
subBlockId: string
isPreview?: boolean
previewValue?: any
disabled?: boolean
}
// Simple mapping UI: for each field in child Input Trigger's inputFormat, render an input with TagDropdown support
export function InputMapping({
blockId,
subBlockId,
isPreview = false,
previewValue,
disabled = false,
}: InputMappingProps) {
const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId)
const [selectedWorkflowId] = useSubBlockValue(blockId, 'workflowId')
const { workflows } = useWorkflowRegistry.getState()
// Fetch child workflow state via registry API endpoint, using cached metadata when possible
// Here we rely on live store; the serializer/executor will resolve at runtime too.
// We only need the inputFormat from an Input Trigger in the selected child workflow state.
const [childInputFields, setChildInputFields] = useState<Array<{ name: string; type?: string }>>(
[]
)
useEffect(() => {
let isMounted = true
const controller = new AbortController()
async function fetchChildSchema() {
try {
if (!selectedWorkflowId) {
if (isMounted) setChildInputFields([])
return
}
const res = await fetch(`/api/workflows/${selectedWorkflowId}`, {
signal: controller.signal,
})
if (!res.ok) {
if (isMounted) setChildInputFields([])
return
}
const { data } = await res.json()
const blocks = (data?.state?.blocks as Record<string, unknown>) || {}
// Prefer new input_trigger
const triggerEntry = Object.entries(blocks).find(([, b]) => isInputTriggerBlock(b))
if (triggerEntry && isInputTriggerBlock(triggerEntry[1])) {
const inputFormat = triggerEntry[1].subBlocks?.inputFormat?.value
if (Array.isArray(inputFormat)) {
const fields = (inputFormat as unknown[])
.filter(isInputFormatField)
.map((f) => ({ name: f.name, type: f.type }))
if (isMounted) setChildInputFields(fields)
return
}
}
// Fallback: legacy starter block inputFormat (subBlocks or config.params)
const starterEntry = Object.entries(blocks).find(([, b]) => isStarterBlock(b))
if (starterEntry && isStarterBlock(starterEntry[1])) {
const starter = starterEntry[1]
const subBlockFormat = starter.subBlocks?.inputFormat?.value
const legacyParamsFormat = starter.config?.params?.inputFormat
const chosen = Array.isArray(subBlockFormat) ? subBlockFormat : legacyParamsFormat
if (Array.isArray(chosen)) {
const fields = (chosen as unknown[])
.filter(isInputFormatField)
.map((f) => ({ name: f.name, type: f.type }))
if (isMounted) setChildInputFields(fields)
return
}
}
if (isMounted) setChildInputFields([])
} catch {
if (isMounted) setChildInputFields([])
}
}
fetchChildSchema()
return () => {
isMounted = false
controller.abort()
}
}, [selectedWorkflowId])
const valueObj: Record<string, any> = useMemo(() => {
if (isPreview && previewValue && typeof previewValue === 'object') return previewValue
if (mapping && typeof mapping === 'object') return mapping as Record<string, any>
try {
if (typeof mapping === 'string') return JSON.parse(mapping)
} catch {}
return {}
}, [mapping, isPreview, previewValue])
const update = (field: string, value: string) => {
if (disabled) return
const updated = { ...valueObj, [field]: value }
setMapping(updated)
}
if (!selectedWorkflowId) {
return (
<div className='flex flex-col items-center justify-center rounded-lg border border-border/50 bg-muted/30 p-8 text-center'>
<svg
className='mb-3 h-10 w-10 text-muted-foreground/60'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={1.5}
d='M13 10V3L4 14h7v7l9-11h-7z'
/>
</svg>
<p className='font-medium text-muted-foreground text-sm'>No workflow selected</p>
<p className='mt-1 text-muted-foreground/80 text-xs'>
Select a workflow above to configure inputs
</p>
</div>
)
}
if (!childInputFields || childInputFields.length === 0) {
return (
<div className='flex flex-col items-center justify-center rounded-lg border border-border/50 bg-muted/30 p-8 text-center'>
<svg
className='mb-3 h-10 w-10 text-muted-foreground/60'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={1.5}
d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'
/>
</svg>
<p className='font-medium text-muted-foreground text-sm'>No input fields defined</p>
<p className='mt-1 max-w-[200px] text-muted-foreground/80 text-xs'>
The selected workflow needs an Input Trigger with defined fields
</p>
</div>
)
}
return (
<div className='space-y-4'>
{childInputFields.map((field) => {
return (
<InputMappingField
key={field.name}
fieldName={field.name}
fieldType={field.type}
value={valueObj[field.name] || ''}
onChange={(value) => update(field.name, value)}
blockId={blockId}
subBlockId={subBlockId}
disabled={isPreview || disabled}
/>
)
})}
</div>
)
}
// Individual field component with TagDropdown support
function InputMappingField({
fieldName,
fieldType,
value,
onChange,
blockId,
subBlockId,
disabled,
}: {
fieldName: string
fieldType?: string
value: string
onChange: (value: string) => void
blockId: string
subBlockId: string
disabled: boolean
}) {
const [showTags, setShowTags] = useState(false)
const [cursorPosition, setCursorPosition] = useState(0)
const inputRef = useRef<HTMLInputElement>(null)
const overlayRef = useRef<HTMLDivElement>(null)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) {
e.preventDefault()
return
}
const newValue = e.target.value
const newCursorPosition = e.target.selectionStart ?? 0
onChange(newValue)
setCursorPosition(newCursorPosition)
// Check for tag trigger
const tagTrigger = checkTagTrigger(newValue, newCursorPosition)
setShowTags(tagTrigger.show)
}
// Sync scroll position between input and overlay
const handleScroll = (e: React.UIEvent<HTMLInputElement>) => {
if (overlayRef.current) {
overlayRef.current.scrollLeft = e.currentTarget.scrollLeft
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
setShowTags(false)
}
}
const handleTagSelect = (newValue: string) => {
onChange(newValue)
}
return (
<div className='group relative rounded-lg border border-border/50 bg-background/50 p-3 transition-all hover:border-border hover:bg-background'>
<div className='mb-2 flex items-center justify-between'>
<Label className='font-medium text-foreground text-xs'>{fieldName}</Label>
{fieldType && (
<span className='rounded-md bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground'>
{fieldType}
</span>
)}
</div>
<div className='relative w-full'>
<Input
ref={inputRef}
className={cn(
'allow-scroll h-9 w-full overflow-auto border-0 bg-muted/50 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus:bg-background',
'transition-colors duration-200'
)}
type='text'
value={value}
onChange={handleChange}
onFocus={() => {
setShowTags(false)
}}
onBlur={() => {
setShowTags(false)
}}
onScroll={handleScroll}
onKeyDown={handleKeyDown}
autoComplete='off'
style={{ overflowX: 'auto' }}
disabled={disabled}
/>
<div
ref={overlayRef}
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-3 text-sm'
style={{ overflowX: 'auto' }}
>
<div
className='w-full whitespace-pre'
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
>
{formatDisplayText(value)}
</div>
</div>
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId={blockId}
activeSourceBlockId={null}
inputValue={value}
cursorPosition={cursorPosition}
onClose={() => {
setShowTags(false)
}}
/>
</div>
</div>
)
}

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { PlusIcon, Server, WrenchIcon, XIcon } from 'lucide-react' import { AlertCircle, PlusIcon, Server, WrenchIcon, XIcon } from 'lucide-react'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
@@ -374,6 +374,44 @@ function FileUploadSyncWrapper({
) )
} }
// Error boundary component for tool input
class ToolInputErrorBoundary extends React.Component<
{ children: React.ReactNode; blockName?: string },
{ hasError: boolean; error?: Error }
> {
constructor(props: any) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error }
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('ToolInput error:', error, info)
}
render() {
if (this.state.hasError) {
return (
<div className='rounded-md bg-red-50 p-4 text-red-800 text-sm dark:bg-red-900/20 dark:text-red-200'>
<div className='flex items-center gap-2'>
<AlertCircle className='h-4 w-4' />
<span className='font-medium'>Tool Configuration Error</span>
</div>
<p className='mt-1 text-xs opacity-80'>
{this.props.blockName ? `Block "${this.props.blockName}": ` : ''}
Invalid tool reference. Please check the workflow configuration.
</p>
</div>
)
}
return this.props.children
}
}
export function ToolInput({ export function ToolInput({
blockId, blockId,
subBlockId, subBlockId,
@@ -475,10 +513,18 @@ export function ToolInput({
// Fallback: create options from tools.access // Fallback: create options from tools.access
return block.tools.access.map((toolId) => { return block.tools.access.map((toolId) => {
const toolParams = getToolParametersConfig(toolId) try {
return { const toolParams = getToolParametersConfig(toolId)
id: toolId, return {
label: toolParams?.toolConfig?.name || toolId, id: toolId,
label: toolParams?.toolConfig?.name || toolId,
}
} catch (error) {
console.error(`Error getting tool config for ${toolId}:`, error)
return {
id: toolId,
label: toolId,
}
} }
}) })
} }

View File

@@ -18,6 +18,7 @@ import {
FileUpload, FileUpload,
FolderSelectorInput, FolderSelectorInput,
InputFormat, InputFormat,
InputMapping,
KnowledgeBaseSelector, KnowledgeBaseSelector,
LongInput, LongInput,
McpDynamicArgs, McpDynamicArgs,
@@ -450,6 +451,17 @@ export function SubBlock({
/> />
) )
} }
case 'input-mapping': {
return (
<InputMapping
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
previewValue={previewValue}
disabled={isDisabled}
/>
)
}
case 'response-format': case 'response-format':
return ( return (
<ResponseFormat <ResponseFormat

View File

@@ -141,11 +141,32 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
isShowingDiff, isShowingDiff,
id, id,
]) ])
// Always call hooks to maintain consistent hook order
const storeHorizontalHandles = useWorkflowStore(
(state) => state.blocks[id]?.horizontalHandles ?? true
)
const storeIsWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false)
const storeBlockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0)
const storeBlockAdvancedMode = useWorkflowStore(
(state) => state.blocks[id]?.advancedMode ?? false
)
const storeBlockTriggerMode = useWorkflowStore((state) => state.blocks[id]?.triggerMode ?? false)
// Get block properties from currentWorkflow when in diff mode, otherwise from workflow store
const horizontalHandles = data.isPreview const horizontalHandles = data.isPreview
? (data.blockState?.horizontalHandles ?? true) // In preview mode, use blockState and default to horizontal ? (data.blockState?.horizontalHandles ?? true) // In preview mode, use blockState and default to horizontal
: useWorkflowStore((state) => state.blocks[id]?.horizontalHandles ?? true) // Changed default to true for consistency : currentWorkflow.isDiffMode
const isWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false) ? (currentWorkflow.blocks[id]?.horizontalHandles ?? true)
const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0) : storeHorizontalHandles
const isWide = currentWorkflow.isDiffMode
? (currentWorkflow.blocks[id]?.isWide ?? false)
: storeIsWide
const blockHeight = currentWorkflow.isDiffMode
? (currentWorkflow.blocks[id]?.height ?? 0)
: storeBlockHeight
// Get per-block webhook status by checking if webhook is configured // Get per-block webhook status by checking if webhook is configured
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
@@ -157,8 +178,14 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
) )
const blockWebhookStatus = !!(hasWebhookProvider && hasWebhookPath) const blockWebhookStatus = !!(hasWebhookProvider && hasWebhookPath)
const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false) const blockAdvancedMode = currentWorkflow.isDiffMode
const blockTriggerMode = useWorkflowStore((state) => state.blocks[id]?.triggerMode ?? false) ? (currentWorkflow.blocks[id]?.advancedMode ?? false)
: storeBlockAdvancedMode
// Get triggerMode from currentWorkflow blocks when in diff mode, otherwise from workflow store
const blockTriggerMode = currentWorkflow.isDiffMode
? (currentWorkflow.blocks[id]?.triggerMode ?? false)
: storeBlockTriggerMode
// Local UI state for diff mode controls // Local UI state for diff mode controls
const [diffIsWide, setDiffIsWide] = useState<boolean>(isWide) const [diffIsWide, setDiffIsWide] = useState<boolean>(isWide)
@@ -660,7 +687,10 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
{/* Block Header */} {/* Block Header */}
<div <div
className='workflow-drag-handle flex cursor-grab items-center justify-between border-b p-3 [&:active]:cursor-grabbing' className={cn(
'workflow-drag-handle flex cursor-grab items-center justify-between p-3 [&:active]:cursor-grabbing',
subBlockRows.length > 0 && 'border-b'
)}
onMouseDown={(e) => { onMouseDown={(e) => {
e.stopPropagation() e.stopPropagation()
}} }}
@@ -891,7 +921,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
<p className='mb-1 font-medium text-sm'>Description</p> <p className='mb-1 font-medium text-sm'>Description</p>
<p className='text-muted-foreground text-sm'>{config.longDescription}</p> <p className='text-muted-foreground text-sm'>{config.longDescription}</p>
</div> </div>
{config.outputs && ( {config.outputs && Object.keys(config.outputs).length > 0 && (
<div> <div>
<p className='mb-1 font-medium text-sm'>Output</p> <p className='mb-1 font-medium text-sm'>Output</p>
<div className='text-sm'> <div className='text-sm'>
@@ -929,90 +959,92 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
</Tooltip> </Tooltip>
) )
)} )}
<Tooltip> {subBlockRows.length > 0 && (
<TooltipTrigger asChild> <Tooltip>
<Button <TooltipTrigger asChild>
variant='ghost' <Button
size='sm' variant='ghost'
onClick={() => { size='sm'
if (currentWorkflow.isDiffMode) { onClick={() => {
setDiffIsWide((prev) => !prev) if (currentWorkflow.isDiffMode) {
} else if (userPermissions.canEdit) { setDiffIsWide((prev) => !prev)
collaborativeToggleBlockWide(id) } else if (userPermissions.canEdit) {
} collaborativeToggleBlockWide(id)
}} }
className={cn( }}
'h-7 p-1 text-gray-500', className={cn(
!userPermissions.canEdit && 'h-7 p-1 text-gray-500',
!currentWorkflow.isDiffMode && !userPermissions.canEdit &&
'cursor-not-allowed opacity-50' !currentWorkflow.isDiffMode &&
)} 'cursor-not-allowed opacity-50'
disabled={!userPermissions.canEdit && !currentWorkflow.isDiffMode} )}
> disabled={!userPermissions.canEdit && !currentWorkflow.isDiffMode}
{displayIsWide ? ( >
<RectangleHorizontal className='h-5 w-5' /> {displayIsWide ? (
) : ( <RectangleHorizontal className='h-5 w-5' />
<RectangleVertical className='h-5 w-5' /> ) : (
)} <RectangleVertical className='h-5 w-5' />
</Button> )}
</TooltipTrigger> </Button>
<TooltipContent side='top'> </TooltipTrigger>
{!userPermissions.canEdit && !currentWorkflow.isDiffMode <TooltipContent side='top'>
? userPermissions.isOfflineMode {!userPermissions.canEdit && !currentWorkflow.isDiffMode
? 'Connection lost - please refresh' ? userPermissions.isOfflineMode
: 'Read-only mode' ? 'Connection lost - please refresh'
: displayIsWide : 'Read-only mode'
? 'Narrow Block' : displayIsWide
: 'Expand Block'} ? 'Narrow Block'
</TooltipContent> : 'Expand Block'}
</Tooltip> </TooltipContent>
</Tooltip>
)}
</div> </div>
</div> </div>
{/* Block Content */} {/* Block Content - Only render if there are subblocks */}
<div {subBlockRows.length > 0 && (
ref={contentRef} <div
className='cursor-pointer space-y-4 px-4 pt-3 pb-4' ref={contentRef}
onMouseDown={(e) => { className='cursor-pointer space-y-4 px-4 pt-3 pb-4'
e.stopPropagation() onMouseDown={(e) => {
}} e.stopPropagation()
> }}
{subBlockRows.length > 0 >
? subBlockRows.map((row, rowIndex) => ( {subBlockRows.map((row, rowIndex) => (
<div key={`row-${rowIndex}`} className='flex gap-4'> <div key={`row-${rowIndex}`} className='flex gap-4'>
{row.map((subBlock, blockIndex) => ( {row.map((subBlock, blockIndex) => (
<div <div
key={`${id}-${rowIndex}-${blockIndex}`} key={`${id}-${rowIndex}-${blockIndex}`}
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')} className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}
> >
<SubBlock <SubBlock
blockId={id} blockId={id}
config={subBlock} config={subBlock}
isConnecting={isConnecting} isConnecting={isConnecting}
isPreview={data.isPreview || currentWorkflow.isDiffMode} isPreview={data.isPreview || currentWorkflow.isDiffMode}
subBlockValues={ subBlockValues={
data.subBlockValues || data.subBlockValues ||
(currentWorkflow.isDiffMode && currentBlock (currentWorkflow.isDiffMode && currentBlock
? (currentBlock as any).subBlocks ? (currentBlock as any).subBlocks
: undefined) : undefined)
} }
disabled={!userPermissions.canEdit} disabled={!userPermissions.canEdit}
fieldDiffStatus={ fieldDiffStatus={
fieldDiff?.changed_fields?.includes(subBlock.id) fieldDiff?.changed_fields?.includes(subBlock.id)
? 'changed' ? 'changed'
: fieldDiff?.unchanged_fields?.includes(subBlock.id) : fieldDiff?.unchanged_fields?.includes(subBlock.id)
? 'unchanged' ? 'unchanged'
: undefined : undefined
} }
allowExpandInPreview={currentWorkflow.isDiffMode} allowExpandInPreview={currentWorkflow.isDiffMode}
isWide={displayIsWide} isWide={displayIsWide}
/> />
</div> </div>
))} ))}
</div> </div>
)) ))}
: null} </div>
</div> )}
{/* Output Handle */} {/* Output Handle */}
{type !== 'condition' && type !== 'response' && ( {type !== 'condition' && type !== 'response' && (

View File

@@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import { processStreamingBlockLogs } from '@/lib/tokenization' import { processStreamingBlockLogs } from '@/lib/tokenization'
import { getBlock } from '@/blocks' import { TriggerUtils } from '@/lib/workflows/triggers'
import type { BlockOutput } from '@/blocks/types' import type { BlockOutput } from '@/blocks/types'
import { Executor } from '@/executor' import { Executor } from '@/executor'
import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types' import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types'
@@ -447,7 +447,30 @@ export function useWorkflowExecution() {
) )
} }
} catch (error: any) { } catch (error: any) {
controller.error(error) // Create a proper error result for logging
const errorResult = {
success: false,
error: error.message || 'Workflow execution failed',
output: {},
logs: [],
metadata: {
duration: 0,
startTime: new Date().toISOString(),
source: 'chat' as const,
},
}
// Send the error as final event so downstream handlers can treat it uniformly
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ event: 'final', data: errorResult })}\n\n`)
)
// Persist the error to logs so it shows up in the logs page
persistLogs(executionId, errorResult).catch((err) =>
logger.error('Error persisting error logs:', err)
)
// Do not error the controller to allow consumers to process the final event
} finally { } finally {
controller.close() controller.close()
setIsExecuting(false) setIsExecuting(false)
@@ -560,22 +583,14 @@ export function useWorkflowExecution() {
} }
}) })
// Filter out trigger blocks for manual execution // Do not filter out trigger blocks; executor may need to start from them
const filteredStates = Object.entries(mergedStates).reduce( const filteredStates = Object.entries(mergedStates).reduce(
(acc, [id, block]) => { (acc, [id, block]) => {
// Skip blocks with undefined type
if (!block || !block.type) { if (!block || !block.type) {
logger.warn(`Skipping block with undefined type: ${id}`, block) logger.warn(`Skipping block with undefined type: ${id}`, block)
return acc return acc
} }
acc[id] = block
const blockConfig = getBlock(block.type)
const isTriggerBlock = blockConfig?.category === 'triggers'
// Skip trigger blocks during manual execution
if (!isTriggerBlock) {
acc[id] = block
}
return acc return acc
}, },
{} as typeof mergedStates {} as typeof mergedStates
@@ -632,15 +647,8 @@ export function useWorkflowExecution() {
{} as Record<string, any> {} as Record<string, any>
) )
// Filter edges to exclude connections to/from trigger blocks // Keep edges intact to allow execution starting from trigger blocks
const triggerBlockIds = Object.keys(mergedStates).filter((id) => { const filteredEdges = workflowEdges
const blockConfig = getBlock(mergedStates[id].type)
return blockConfig?.category === 'triggers'
})
const filteredEdges = workflowEdges.filter(
(edge) => !triggerBlockIds.includes(edge.source) && !triggerBlockIds.includes(edge.target)
)
// Derive subflows from the current filtered graph to avoid stale state // Derive subflows from the current filtered graph to avoid stale state
const runtimeLoops = generateLoopBlocks(filteredStates) const runtimeLoops = generateLoopBlocks(filteredStates)
@@ -663,12 +671,158 @@ export function useWorkflowExecution() {
selectedOutputIds = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId) selectedOutputIds = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId)
} }
// Create executor options // Determine start block and workflow input based on execution type
let startBlockId: string | undefined
let finalWorkflowInput = workflowInput
if (isExecutingFromChat) {
// For chat execution, find the appropriate chat trigger
const startBlock = TriggerUtils.findStartBlock(filteredStates, 'chat')
if (!startBlock) {
throw new Error(TriggerUtils.getTriggerValidationMessage('chat', 'missing'))
}
startBlockId = startBlock.blockId
} else {
// For manual editor runs: look for Manual trigger OR API trigger
const entries = Object.entries(filteredStates)
// Find manual triggers and API triggers
const manualTriggers = TriggerUtils.findTriggersByType(filteredStates, 'manual')
const apiTriggers = TriggerUtils.findTriggersByType(filteredStates, 'api')
logger.info('Manual run trigger check:', {
manualTriggersCount: manualTriggers.length,
apiTriggersCount: apiTriggers.length,
manualTriggers: manualTriggers.map((t) => ({
type: t.type,
name: t.name,
isLegacy: t.type === 'starter',
})),
apiTriggers: apiTriggers.map((t) => ({
type: t.type,
name: t.name,
isLegacy: t.type === 'starter',
})),
})
let selectedTrigger: any = null
let selectedBlockId: string | null = null
// Check for API triggers first (they take precedence over manual triggers)
if (apiTriggers.length === 1) {
selectedTrigger = apiTriggers[0]
const blockEntry = entries.find(([, block]) => block === selectedTrigger)
if (blockEntry) {
selectedBlockId = blockEntry[0]
// Extract test values from the API trigger's inputFormat
if (selectedTrigger.type === 'api_trigger' || selectedTrigger.type === 'starter') {
const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value
if (Array.isArray(inputFormatValue)) {
const testInput: Record<string, any> = {}
inputFormatValue.forEach((field: any) => {
if (field && typeof field === 'object' && field.name && field.value !== undefined) {
testInput[field.name] = field.value
}
})
// Use the test input as workflow input
if (Object.keys(testInput).length > 0) {
finalWorkflowInput = testInput
logger.info('Using API trigger test values for manual run:', testInput)
}
}
}
}
} else if (apiTriggers.length > 1) {
const error = new Error('Multiple API Trigger blocks found. Keep only one.')
logger.error('Multiple API triggers found')
setIsExecuting(false)
throw error
} else if (manualTriggers.length === 1) {
// No API trigger, check for manual trigger
selectedTrigger = manualTriggers[0]
const blockEntry = entries.find(([, block]) => block === selectedTrigger)
if (blockEntry) {
selectedBlockId = blockEntry[0]
}
} else if (manualTriggers.length > 1) {
const error = new Error('Multiple Input Trigger blocks found. Keep only one.')
logger.error('Multiple input triggers found')
setIsExecuting(false)
throw error
} else {
// Fallback: Check for legacy starter block
const starterBlock = Object.values(filteredStates).find((block) => block.type === 'starter')
if (starterBlock) {
// Found a legacy starter block, use it as a manual trigger
const blockEntry = Object.entries(filteredStates).find(
([, block]) => block === starterBlock
)
if (blockEntry) {
selectedBlockId = blockEntry[0]
selectedTrigger = starterBlock
logger.info('Using legacy starter block for manual run')
}
}
if (!selectedBlockId || !selectedTrigger) {
const error = new Error('Manual run requires an Input Trigger or API Trigger block')
logger.error('No input or API triggers found for manual run')
setIsExecuting(false)
throw error
}
}
if (selectedBlockId && selectedTrigger) {
startBlockId = selectedBlockId
// Check if the trigger has any outgoing connections (except for legacy starter blocks)
// Legacy starter blocks have their own validation in the executor
if (selectedTrigger.type !== 'starter') {
const outgoingConnections = workflowEdges.filter((edge) => edge.source === startBlockId)
if (outgoingConnections.length === 0) {
const triggerName = selectedTrigger.name || selectedTrigger.type
const error = new Error(`${triggerName} must be connected to other blocks to execute`)
logger.error('Trigger has no outgoing connections', { triggerName, startBlockId })
setIsExecuting(false)
throw error
}
}
logger.info('Trigger found for manual run:', {
startBlockId,
triggerType: selectedTrigger.type,
triggerName: selectedTrigger.name,
isLegacyStarter: selectedTrigger.type === 'starter',
usingTestValues: selectedTrigger.type === 'api_trigger',
})
}
}
// If we don't have a valid startBlockId at this point, throw an error
if (!startBlockId) {
const error = new Error('No valid trigger block found to start execution')
logger.error('No startBlockId found after trigger search')
setIsExecuting(false)
throw error
}
// Log the final startBlockId
logger.info('Final execution setup:', {
startBlockId,
isExecutingFromChat,
hasWorkflowInput: !!workflowInput,
})
// Create executor options with the final workflow input
const executorOptions: ExecutorOptions = { const executorOptions: ExecutorOptions = {
workflow, workflow,
currentBlockStates, currentBlockStates,
envVarValues, envVarValues,
workflowInput, workflowInput: finalWorkflowInput,
workflowVariables, workflowVariables,
contextExtensions: { contextExtensions: {
stream: isExecutingFromChat, stream: isExecutingFromChat,
@@ -687,8 +841,8 @@ export function useWorkflowExecution() {
const newExecutor = new Executor(executorOptions) const newExecutor = new Executor(executorOptions)
setExecutor(newExecutor) setExecutor(newExecutor)
// Execute workflow // Execute workflow with the determined start block
return newExecutor.execute(activeWorkflowId || '') return newExecutor.execute(activeWorkflowId || '', startBlockId)
} }
const handleExecutionError = (error: any, options?: { executionId?: string }) => { const handleExecutionError = (error: any, options?: { executionId?: string }) => {
@@ -729,7 +883,7 @@ export function useWorkflowExecution() {
try { try {
// Prefer attributing to specific subflow if we have a structured error // Prefer attributing to specific subflow if we have a structured error
let blockId = 'serialization' let blockId = 'serialization'
let blockName = 'Serialization' let blockName = 'Workflow'
let blockType = 'serializer' let blockType = 'serializer'
if (error instanceof WorkflowValidationError) { if (error instanceof WorkflowValidationError) {
blockId = error.blockId || blockId blockId = error.blockId || blockId

View File

@@ -6,7 +6,6 @@
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import { getBlock } from '@/blocks'
import type { BlockOutput } from '@/blocks/types' import type { BlockOutput } from '@/blocks/types'
import { Executor } from '@/executor' import { Executor } from '@/executor'
import type { ExecutionResult, StreamingExecution } from '@/executor/types' import type { ExecutionResult, StreamingExecution } from '@/executor/types'
@@ -131,26 +130,9 @@ export async function executeWorkflowWithLogging(
// Merge subblock states from the appropriate store // Merge subblock states from the appropriate store
const mergedStates = mergeSubblockState(validBlocks) const mergedStates = mergeSubblockState(validBlocks)
// Filter out trigger blocks for manual execution // Don't filter out trigger blocks - let the executor handle them properly
const filteredStates = Object.entries(mergedStates).reduce( // The standard executor has TriggerBlockHandler that knows how to handle triggers
(acc, [id, block]) => { const filteredStates = mergedStates
// Skip blocks with undefined type
if (!block || !block.type) {
logger.warn(`Skipping block with undefined type: ${id}`, block)
return acc
}
const blockConfig = getBlock(block.type)
const isTriggerBlock = blockConfig?.category === 'triggers'
// Skip trigger blocks during manual execution
if (!isTriggerBlock) {
acc[id] = block
}
return acc
},
{} as typeof mergedStates
)
const currentBlockStates = Object.entries(filteredStates).reduce( const currentBlockStates = Object.entries(filteredStates).reduce(
(acc, [id, block]) => { (acc, [id, block]) => {
@@ -186,15 +168,9 @@ export async function executeWorkflowWithLogging(
{} as Record<string, any> {} as Record<string, any>
) )
// Filter edges to exclude connections to/from trigger blocks // Don't filter edges - let all connections remain intact
const triggerBlockIds = Object.keys(mergedStates).filter((id) => { // The executor's routing system will handle execution paths properly
const blockConfig = getBlock(mergedStates[id].type) const filteredEdges = workflowEdges
return blockConfig?.category === 'triggers'
})
const filteredEdges = workflowEdges.filter(
(edge: any) => !triggerBlockIds.includes(edge.source) && !triggerBlockIds.includes(edge.target)
)
// Create serialized workflow with filtered blocks and edges // Create serialized workflow with filtered blocks and edges
const workflow = new Serializer().serializeWorkflow( const workflow = new Serializer().serializeWorkflow(

View File

@@ -206,6 +206,18 @@ export async function applyAutoLayoutAndUpdateStore(
loops: newWorkflowState.loops || {}, loops: newWorkflowState.loops || {},
parallels: newWorkflowState.parallels || {}, parallels: newWorkflowState.parallels || {},
deploymentStatuses: newWorkflowState.deploymentStatuses || {}, deploymentStatuses: newWorkflowState.deploymentStatuses || {},
// Sanitize edges: remove null/empty handle fields to satisfy schema (optional strings)
edges: (newWorkflowState.edges || []).map((edge: any) => {
const { sourceHandle, targetHandle, ...rest } = edge || {}
const sanitized: any = { ...rest }
if (typeof sourceHandle === 'string' && sourceHandle.length > 0) {
sanitized.sourceHandle = sourceHandle
}
if (typeof targetHandle === 'string' && targetHandle.length > 0) {
sanitized.targetHandle = targetHandle
}
return sanitized
}),
} }
// Save the updated workflow state to the database // Save the updated workflow state to the database

View File

@@ -13,6 +13,7 @@ import ReactFlow, {
} from 'reactflow' } from 'reactflow'
import 'reactflow/dist/style.css' import 'reactflow/dist/style.css'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { TriggerUtils } from '@/lib/workflows/triggers'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { ControlBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar' import { ControlBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar'
import { DiffControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls' import { DiffControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls'
@@ -20,6 +21,11 @@ import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp
import { FloatingControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/floating-controls/floating-controls' import { FloatingControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/floating-controls/floating-controls'
import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel' import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel'
import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node'
import { TriggerList } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-list/trigger-list'
import {
TriggerWarningDialog,
TriggerWarningType,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog'
import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
@@ -77,6 +83,17 @@ const WorkflowContent = React.memo(() => {
// Enhanced edge selection with parent context and unique identifier // Enhanced edge selection with parent context and unique identifier
const [selectedEdgeInfo, setSelectedEdgeInfo] = useState<SelectedEdgeInfo | null>(null) const [selectedEdgeInfo, setSelectedEdgeInfo] = useState<SelectedEdgeInfo | null>(null)
// State for trigger warning dialog
const [triggerWarning, setTriggerWarning] = useState<{
open: boolean
triggerName: string
type: TriggerWarningType
}>({
open: false,
triggerName: '',
type: TriggerWarningType.DUPLICATE_TRIGGER,
})
// Hooks // Hooks
const params = useParams() const params = useParams()
const router = useRouter() const router = useRouter()
@@ -107,6 +124,11 @@ const WorkflowContent = React.memo(() => {
// Extract workflow data from the abstraction // Extract workflow data from the abstraction
const { blocks, edges, loops, parallels, isDiffMode } = currentWorkflow const { blocks, edges, loops, parallels, isDiffMode } = currentWorkflow
// Check if workflow is empty (no blocks)
const isWorkflowEmpty = useMemo(() => {
return Object.keys(blocks).length === 0
}, [blocks])
// Get diff analysis for edge reconstruction // Get diff analysis for edge reconstruction
const { diffAnalysis, isShowingDiff, isDiffReady } = useWorkflowDiffStore() const { diffAnalysis, isShowingDiff, isDiffReady } = useWorkflowDiffStore()
@@ -565,7 +587,7 @@ const WorkflowContent = React.memo(() => {
return return
} }
const { type } = event.detail const { type, enableTriggerMode } = event.detail
if (!type) return if (!type) return
if (type === 'connectionBlock') return if (type === 'connectionBlock') return
@@ -637,7 +659,10 @@ const WorkflowContent = React.memo(() => {
// Create a new block with a unique ID // Create a new block with a unique ID
const id = crypto.randomUUID() const id = crypto.randomUUID()
const name = getUniqueBlockName(blockConfig.name, blocks) // Prefer semantic default names for triggers; then ensure unique numbering centrally
const defaultTriggerName = TriggerUtils.getDefaultTriggerName(type)
const baseName = defaultTriggerName || blockConfig.name
const name = getUniqueBlockName(baseName, blocks)
// Auto-connect logic // Auto-connect logic
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
@@ -661,8 +686,38 @@ const WorkflowContent = React.memo(() => {
} }
} }
// Centralized trigger constraints
const additionIssue = TriggerUtils.getTriggerAdditionIssue(blocks, type)
if (additionIssue) {
if (additionIssue.issue === 'legacy') {
setTriggerWarning({
open: true,
triggerName: additionIssue.triggerName,
type: TriggerWarningType.LEGACY_INCOMPATIBILITY,
})
} else {
setTriggerWarning({
open: true,
triggerName: additionIssue.triggerName,
type: TriggerWarningType.DUPLICATE_TRIGGER,
})
}
return
}
// Add the block to the workflow with auto-connect edge // Add the block to the workflow with auto-connect edge
addBlock(id, type, name, centerPosition, undefined, undefined, undefined, autoConnectEdge) // Enable trigger mode if this is a trigger-capable block from the triggers tab
addBlock(
id,
type,
name,
centerPosition,
undefined,
undefined,
undefined,
autoConnectEdge,
enableTriggerMode
)
} }
window.addEventListener('add-block-from-toolbar', handleAddBlockFromToolbar as EventListener) window.addEventListener('add-block-from-toolbar', handleAddBlockFromToolbar as EventListener)
@@ -681,8 +736,35 @@ const WorkflowContent = React.memo(() => {
findClosestOutput, findClosestOutput,
determineSourceHandle, determineSourceHandle,
effectivePermissions.canEdit, effectivePermissions.canEdit,
setTriggerWarning,
]) ])
// Handler for trigger selection from list
const handleTriggerSelect = useCallback(
(triggerId: string, enableTriggerMode?: boolean) => {
// Get the trigger name
const triggerName = TriggerUtils.getDefaultTriggerName(triggerId) || triggerId
// Create the trigger block at the center of the viewport
const centerPosition = project({ x: window.innerWidth / 2, y: window.innerHeight / 2 })
const id = `${triggerId}_${Date.now()}`
// Add the trigger block with trigger mode if specified
addBlock(
id,
triggerId,
triggerName,
centerPosition,
undefined,
undefined,
undefined,
undefined,
enableTriggerMode || false
)
},
[project, addBlock]
)
// Update the onDrop handler // Update the onDrop handler
const onDrop = useCallback( const onDrop = useCallback(
(event: React.DragEvent) => { (event: React.DragEvent) => {
@@ -784,8 +866,14 @@ const WorkflowContent = React.memo(() => {
// Generate id and name here so they're available in all code paths // Generate id and name here so they're available in all code paths
const id = crypto.randomUUID() const id = crypto.randomUUID()
// Prefer semantic default names for triggers; then ensure unique numbering centrally
const defaultTriggerNameDrop = TriggerUtils.getDefaultTriggerName(data.type)
const baseName = const baseName =
data.type === 'loop' ? 'Loop' : data.type === 'parallel' ? 'Parallel' : blockConfig!.name data.type === 'loop'
? 'Loop'
: data.type === 'parallel'
? 'Parallel'
: defaultTriggerNameDrop || blockConfig!.name
const name = getUniqueBlockName(baseName, blocks) const name = getUniqueBlockName(baseName, blocks)
if (containerInfo) { if (containerInfo) {
@@ -868,6 +956,20 @@ const WorkflowContent = React.memo(() => {
// Immediate resize without delay // Immediate resize without delay
resizeLoopNodesWrapper() resizeLoopNodesWrapper()
} else { } else {
// Centralized trigger constraints
const dropIssue = TriggerUtils.getTriggerAdditionIssue(blocks, data.type)
if (dropIssue) {
setTriggerWarning({
open: true,
triggerName: dropIssue.triggerName,
type:
dropIssue.issue === 'legacy'
? TriggerWarningType.LEGACY_INCOMPATIBILITY
: TriggerWarningType.DUPLICATE_TRIGGER,
})
return
}
// Regular auto-connect logic // Regular auto-connect logic
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
let autoConnectEdge let autoConnectEdge
@@ -888,7 +990,19 @@ const WorkflowContent = React.memo(() => {
} }
// Regular canvas drop with auto-connect edge // Regular canvas drop with auto-connect edge
addBlock(id, data.type, name, position, undefined, undefined, undefined, autoConnectEdge) // Use enableTriggerMode from drag data if present (when dragging from Triggers tab)
const enableTriggerMode = data.enableTriggerMode || false
addBlock(
id,
data.type,
name,
position,
undefined,
undefined,
undefined,
autoConnectEdge,
enableTriggerMode
)
} }
} catch (err) { } catch (err) {
logger.error('Error dropping block:', { err }) logger.error('Error dropping block:', { err })
@@ -903,6 +1017,7 @@ const WorkflowContent = React.memo(() => {
determineSourceHandle, determineSourceHandle,
isPointInLoopNodeWrapper, isPointInLoopNodeWrapper,
getNodes, getNodes,
setTriggerWarning,
] ]
) )
@@ -1815,6 +1930,19 @@ const WorkflowContent = React.memo(() => {
{/* Show DiffControls if diff is available (regardless of current view mode) */} {/* Show DiffControls if diff is available (regardless of current view mode) */}
<DiffControls /> <DiffControls />
{/* Trigger warning dialog */}
<TriggerWarningDialog
open={triggerWarning.open}
onOpenChange={(open) => setTriggerWarning({ ...triggerWarning, open })}
triggerName={triggerWarning.triggerName}
type={triggerWarning.type}
/>
{/* Trigger list for empty workflows - only show after workflow has loaded */}
{isWorkflowReady && isWorkflowEmpty && effectivePermissions.canEdit && (
<TriggerList onSelect={handleTriggerSelect} />
)}
</div> </div>
</div> </div>
) )

View File

@@ -7,9 +7,14 @@ import type { BlockConfig } from '@/blocks/types'
export type ToolbarBlockProps = { export type ToolbarBlockProps = {
config: BlockConfig config: BlockConfig
disabled?: boolean disabled?: boolean
enableTriggerMode?: boolean
} }
export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) { export function ToolbarBlock({
config,
disabled = false,
enableTriggerMode = false,
}: ToolbarBlockProps) {
const userPermissions = useUserPermissionsContext() const userPermissions = useUserPermissionsContext()
const handleDragStart = (e: React.DragEvent) => { const handleDragStart = (e: React.DragEvent) => {
@@ -17,7 +22,13 @@ export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) {
e.preventDefault() e.preventDefault()
return return
} }
e.dataTransfer.setData('application/json', JSON.stringify({ type: config.type })) e.dataTransfer.setData(
'application/json',
JSON.stringify({
type: config.type,
enableTriggerMode,
})
)
e.dataTransfer.effectAllowed = 'move' e.dataTransfer.effectAllowed = 'move'
} }
@@ -29,10 +40,11 @@ export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) {
const event = new CustomEvent('add-block-from-toolbar', { const event = new CustomEvent('add-block-from-toolbar', {
detail: { detail: {
type: config.type, type: config.type,
enableTriggerMode,
}, },
}) })
window.dispatchEvent(event) window.dispatchEvent(event)
}, [config.type, disabled]) }, [config.type, disabled, enableTriggerMode])
const blockContent = ( const blockContent = (
<div <div

View File

@@ -4,10 +4,15 @@ import { useMemo, useState } from 'react'
import { Search } from 'lucide-react' import { Search } from 'lucide-react'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Tabs, TabsContent } from '@/components/ui/tabs'
import {
getBlocksForSidebar,
getTriggersForSidebar,
hasTriggerCapability,
} from '@/lib/workflows/trigger-utils'
import { ToolbarBlock } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block' import { ToolbarBlock } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block'
import LoopToolbarItem from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-loop-block/toolbar-loop-block' import LoopToolbarItem from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-loop-block/toolbar-loop-block'
import ParallelToolbarItem from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block' import ParallelToolbarItem from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block'
import { getAllBlocks } from '@/blocks'
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions' import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
interface ToolbarProps { interface ToolbarProps {
@@ -24,25 +29,30 @@ interface BlockItem {
export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: ToolbarProps) { export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: ToolbarProps) {
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [activeTab, setActiveTab] = useState('blocks')
const { regularBlocks, specialBlocks, tools, triggers } = useMemo(() => { const { regularBlocks, specialBlocks, tools, triggers } = useMemo(() => {
const allBlocks = getAllBlocks() // Get blocks based on the active tab using centralized logic
const sourceBlocks = activeTab === 'blocks' ? getBlocksForSidebar() : getTriggersForSidebar()
// Filter blocks based on search query // Filter blocks based on search query
const filteredBlocks = allBlocks.filter((block) => { const filteredBlocks = sourceBlocks.filter((block) => {
if (block.type === 'starter' || block.hideFromToolbar) return false const matchesSearch =
return (
!searchQuery.trim() || !searchQuery.trim() ||
block.name.toLowerCase().includes(searchQuery.toLowerCase()) || block.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
block.description.toLowerCase().includes(searchQuery.toLowerCase()) block.description.toLowerCase().includes(searchQuery.toLowerCase())
)
return matchesSearch
}) })
// Separate blocks by category: 'blocks', 'tools', and 'triggers' // Separate blocks by category
const regularBlockConfigs = filteredBlocks.filter((block) => block.category === 'blocks') const regularBlockConfigs = filteredBlocks.filter((block) => block.category === 'blocks')
const toolConfigs = filteredBlocks.filter((block) => block.category === 'tools') const toolConfigs = filteredBlocks.filter((block) => block.category === 'tools')
const triggerConfigs = filteredBlocks.filter((block) => block.category === 'triggers') // For triggers tab, include both 'triggers' category and tools with trigger capability
const triggerConfigs =
activeTab === 'triggers'
? filteredBlocks
: filteredBlocks.filter((block) => block.category === 'triggers')
// Create regular block items and sort alphabetically // Create regular block items and sort alphabetically
const regularBlockItems: BlockItem[] = regularBlockConfigs const regularBlockItems: BlockItem[] = regularBlockConfigs
@@ -54,23 +64,25 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
})) }))
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
// Create special blocks (loop and parallel) if they match search // Create special blocks (loop and parallel) only for blocks tab
const specialBlockItems: BlockItem[] = [] const specialBlockItems: BlockItem[] = []
if (!searchQuery.trim() || 'loop'.toLowerCase().includes(searchQuery.toLowerCase())) { if (activeTab === 'blocks') {
specialBlockItems.push({ if (!searchQuery.trim() || 'loop'.toLowerCase().includes(searchQuery.toLowerCase())) {
name: 'Loop', specialBlockItems.push({
type: 'loop', name: 'Loop',
isCustom: true, type: 'loop',
}) isCustom: true,
} })
}
if (!searchQuery.trim() || 'parallel'.toLowerCase().includes(searchQuery.toLowerCase())) { if (!searchQuery.trim() || 'parallel'.toLowerCase().includes(searchQuery.toLowerCase())) {
specialBlockItems.push({ specialBlockItems.push({
name: 'Parallel', name: 'Parallel',
type: 'parallel', type: 'parallel',
isCustom: true, isCustom: true,
}) })
}
} }
// Sort special blocks alphabetically // Sort special blocks alphabetically
@@ -95,65 +107,106 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
tools: toolConfigs, tools: toolConfigs,
triggers: triggerBlockItems, triggers: triggerBlockItems,
} }
}, [searchQuery]) }, [searchQuery, activeTab])
return ( return (
<div className='flex h-full flex-col'> <div className='flex h-full flex-col'>
{/* Search */} {/* Tabs */}
<div className='flex-shrink-0 p-2'> <Tabs value={activeTab} onValueChange={setActiveTab} className='flex h-full flex-col'>
<div className='flex h-9 items-center gap-2 rounded-[8px] border bg-background pr-2 pl-3'> <div className='flex-shrink-0 px-2 pt-2'>
<Search className='h-4 w-4 text-muted-foreground' strokeWidth={2} /> <div className='flex h-9 w-full items-center gap-1 rounded-[10px] border bg-card px-[2.5px] py-1 shadow-xs'>
<Input <button
placeholder='Search blocks...' onClick={() => setActiveTab('blocks')}
value={searchQuery} className={`panel-tab-base inline-flex flex-1 cursor-pointer items-center justify-center rounded-[8px] border border-transparent py-1 font-[450] text-sm outline-none transition-colors duration-200 ${
onChange={(e) => setSearchQuery(e.target.value)} activeTab === 'blocks' ? 'panel-tab-active' : 'panel-tab-inactive'
className='h-6 flex-1 border-0 bg-transparent px-0 text-muted-foreground text-sm leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0' }`}
autoComplete='off' >
autoCorrect='off' Blocks
autoCapitalize='off' </button>
spellCheck='false' <button
/> onClick={() => setActiveTab('triggers')}
className={`panel-tab-base inline-flex flex-1 cursor-pointer items-center justify-center rounded-[8px] border border-transparent py-1 font-[450] text-sm outline-none transition-colors duration-200 ${
activeTab === 'triggers' ? 'panel-tab-active' : 'panel-tab-inactive'
}`}
>
Triggers
</button>
</div>
</div> </div>
</div>
{/* Content */} {/* Search */}
<ScrollArea className='flex-1 px-2 pb-[0.26px]' hideScrollbar={true}> <div className='flex-shrink-0 p-2'>
<div className='space-y-1 pb-2'> <div className='flex h-9 items-center gap-2 rounded-[8px] border bg-background pr-2 pl-3'>
{/* Regular Blocks Section */} <Search className='h-4 w-4 text-muted-foreground' strokeWidth={2} />
{regularBlocks.map((block) => ( <Input
<ToolbarBlock placeholder={activeTab === 'blocks' ? 'Search blocks...' : 'Search triggers...'}
key={block.type} value={searchQuery}
config={block.config} onChange={(e) => setSearchQuery(e.target.value)}
disabled={!userPermissions.canEdit} className='h-6 flex-1 border-0 bg-transparent px-0 text-muted-foreground text-sm leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
/> />
))} </div>
{/* Special Blocks Section (Loop & Parallel) */}
{specialBlocks.map((block) => {
if (block.type === 'loop') {
return <LoopToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
}
if (block.type === 'parallel') {
return <ParallelToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
}
return null
})}
{/* Triggers Section */}
{triggers.map((trigger) => (
<ToolbarBlock
key={trigger.type}
config={trigger.config}
disabled={!userPermissions.canEdit}
/>
))}
{/* Tools Section */}
{tools.map((tool) => (
<ToolbarBlock key={tool.type} config={tool} disabled={!userPermissions.canEdit} />
))}
</div> </div>
</ScrollArea>
{/* Blocks Tab Content */}
<TabsContent value='blocks' className='mt-0 flex-1 overflow-hidden'>
<ScrollArea className='h-full px-2' hideScrollbar={true}>
<div className='space-y-1 pb-2'>
{/* Regular Blocks */}
{regularBlocks.map((block) => (
<ToolbarBlock
key={block.type}
config={block.config}
disabled={!userPermissions.canEdit}
/>
))}
{/* Special Blocks (Loop & Parallel) */}
{specialBlocks.map((block) => {
if (block.type === 'loop') {
return <LoopToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
}
if (block.type === 'parallel') {
return (
<ParallelToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
)
}
return null
})}
{/* Tools */}
{tools.map((tool) => (
<ToolbarBlock key={tool.type} config={tool} disabled={!userPermissions.canEdit} />
))}
</div>
</ScrollArea>
</TabsContent>
{/* Triggers Tab Content */}
<TabsContent value='triggers' className='mt-0 flex-1 overflow-hidden'>
<ScrollArea className='h-full px-2' hideScrollbar={true}>
<div className='space-y-1 pb-2'>
{triggers.length > 0 ? (
triggers.map((trigger) => (
<ToolbarBlock
key={trigger.type}
config={trigger.config}
disabled={!userPermissions.canEdit}
enableTriggerMode={hasTriggerCapability(trigger.config)}
/>
))
) : (
<div className='py-8 text-center text-muted-foreground text-sm'>
{searchQuery ? 'No triggers found' : 'Add triggers from the workflow canvas'}
</div>
)}
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</div> </div>
) )
} }

View File

@@ -2,6 +2,7 @@ import { AgentIcon } from '@/components/icons'
import { isHosted } from '@/lib/environment' import { isHosted } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { import {
getAllModelProviders, getAllModelProviders,
getBaseModelProviders, getBaseModelProviders,
@@ -61,6 +62,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
type: 'agent', type: 'agent',
name: 'Agent', name: 'Agent',
description: 'Build an agent', description: 'Build an agent',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'The Agent block is a core workflow block that is a wrapper around an LLM. It takes in system/user prompts and calls an LLM provider. It can also make tool calls by directly containing tools inside of its tool input. It can additionally return structured output.', 'The Agent block is a core workflow block that is a wrapper around an LLM. It takes in system/user prompts and calls an LLM provider. It can also make tool calls by directly containing tools inside of its tool input. It can additionally return structured output.',
docsLink: 'https://docs.sim.ai/blocks/agent', docsLink: 'https://docs.sim.ai/blocks/agent',

View File

@@ -1,13 +1,15 @@
import { AirtableIcon } from '@/components/icons' import { AirtableIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { AirtableResponse } from '@/tools/airtable/types' import type { AirtableResponse } from '@/tools/airtable/types'
export const AirtableBlock: BlockConfig<AirtableResponse> = { export const AirtableBlock: BlockConfig<AirtableResponse> = {
type: 'airtable', type: 'airtable',
name: 'Airtable', name: 'Airtable',
description: 'Read, create, and update Airtable', description: 'Read, create, and update Airtable',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrates Airtable into the workflow. Can create, get, list, or update Airtable records. Requires OAuth. Can be used in trigger mode to trigger a workflow when an update is made to an Airtable table.', 'Integrates Airtable into the workflow. Can create, get, list, or update Airtable records. Can be used in trigger mode to trigger a workflow when an update is made to an Airtable table.',
docsLink: 'https://docs.sim.ai/tools/airtable', docsLink: 'https://docs.sim.ai/tools/airtable',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -0,0 +1,34 @@
import { ApiIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
export const ApiTriggerBlock: BlockConfig = {
type: 'api_trigger',
name: 'API',
description: 'Expose as HTTP API endpoint',
longDescription:
'API trigger to start the workflow via authenticated HTTP calls with structured input.',
category: 'triggers',
bgColor: '#2F55FF',
icon: ApiIcon,
subBlocks: [
{
id: 'inputFormat',
title: 'Input Format',
type: 'input-format',
layout: 'full',
description: 'Define the JSON input schema accepted by the API endpoint.',
},
],
tools: {
access: [],
},
inputs: {},
outputs: {
// Dynamic outputs will be added from inputFormat at runtime
// Always includes 'input' field plus any fields defined in inputFormat
},
triggers: {
enabled: true,
available: ['api'],
},
}

View File

@@ -1,13 +1,14 @@
import { BrowserUseIcon } from '@/components/icons' import { BrowserUseIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { BrowserUseResponse } from '@/tools/browser_use/types' import type { BrowserUseResponse } from '@/tools/browser_use/types'
export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = { export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
type: 'browser_use', type: 'browser_use',
name: 'Browser Use', name: 'Browser Use',
description: 'Run browser automation tasks', description: 'Run browser automation tasks',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Browser Use into the workflow. Can navigate the web and perform actions as if a real user was interacting with the browser. Requires API Key.', 'Integrate Browser Use into the workflow. Can navigate the web and perform actions as if a real user was interacting with the browser.',
docsLink: 'https://docs.sim.ai/tools/browser_use', docsLink: 'https://docs.sim.ai/tools/browser_use',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -0,0 +1,30 @@
import type { SVGProps } from 'react'
import { createElement } from 'react'
import { MessageCircle } from 'lucide-react'
import type { BlockConfig } from '@/blocks/types'
const ChatTriggerIcon = (props: SVGProps<SVGSVGElement>) => createElement(MessageCircle, props)
export const ChatTriggerBlock: BlockConfig = {
type: 'chat_trigger',
name: 'Chat',
description: 'Start workflow from a chat deployment',
longDescription: 'Chat trigger to run the workflow via deployed chat interfaces.',
category: 'triggers',
bgColor: '#6F3DFA',
icon: ChatTriggerIcon,
subBlocks: [],
tools: {
access: [],
},
inputs: {},
outputs: {
input: { type: 'string', description: 'User message' },
conversationId: { type: 'string', description: 'Conversation ID' },
files: { type: 'array', description: 'Uploaded files' },
},
triggers: {
enabled: true,
available: ['chat'],
},
}

View File

@@ -1,13 +1,13 @@
import { ClayIcon } from '@/components/icons' import { ClayIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { ClayPopulateResponse } from '@/tools/clay/types' import type { ClayPopulateResponse } from '@/tools/clay/types'
export const ClayBlock: BlockConfig<ClayPopulateResponse> = { export const ClayBlock: BlockConfig<ClayPopulateResponse> = {
type: 'clay', type: 'clay',
name: 'Clay', name: 'Clay',
description: 'Populate Clay workbook', description: 'Populate Clay workbook',
longDescription: authMode: AuthMode.ApiKey,
'Integrate Clay into the workflow. Can populate a table with data. Requires an API Key.', longDescription: 'Integrate Clay into the workflow. Can populate a table with data.',
docsLink: 'https://docs.sim.ai/tools/clay', docsLink: 'https://docs.sim.ai/tools/clay',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,13 +1,14 @@
import { ConfluenceIcon } from '@/components/icons' import { ConfluenceIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { ConfluenceResponse } from '@/tools/confluence/types' import type { ConfluenceResponse } from '@/tools/confluence/types'
export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = { export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
type: 'confluence', type: 'confluence',
name: 'Confluence', name: 'Confluence',
description: 'Interact with Confluence', description: 'Interact with Confluence',
longDescription: authMode: AuthMode.OAuth,
'Integrate Confluence into the workflow. Can read and update a page. Requires OAuth.', longDescription: 'Integrate Confluence into the workflow. Can read and update a page.',
docsLink: 'https://docs.sim.ai/tools/confluence', docsLink: 'https://docs.sim.ai/tools/confluence',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,13 +1,15 @@
import { DiscordIcon } from '@/components/icons' import { DiscordIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { DiscordResponse } from '@/tools/discord/types' import type { DiscordResponse } from '@/tools/discord/types'
export const DiscordBlock: BlockConfig<DiscordResponse> = { export const DiscordBlock: BlockConfig<DiscordResponse> = {
type: 'discord', type: 'discord',
name: 'Discord', name: 'Discord',
description: 'Interact with Discord', description: 'Interact with Discord',
authMode: AuthMode.BotToken,
longDescription: longDescription:
'Integrate Discord into the workflow. Can send and get messages, get server information, and get a users information. Requires bot API key.', 'Integrate Discord into the workflow. Can send and get messages, get server information, and get a users information.',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',
icon: DiscordIcon, icon: DiscordIcon,

View File

@@ -1,13 +1,13 @@
import { ElevenLabsIcon } from '@/components/icons' import { ElevenLabsIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { ElevenLabsBlockResponse } from '@/tools/elevenlabs/types' import type { ElevenLabsBlockResponse } from '@/tools/elevenlabs/types'
export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = { export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
type: 'elevenlabs', type: 'elevenlabs',
name: 'ElevenLabs', name: 'ElevenLabs',
description: 'Convert TTS using ElevenLabs', description: 'Convert TTS using ElevenLabs',
longDescription: authMode: AuthMode.ApiKey,
'Integrate ElevenLabs into the workflow. Can convert text to speech. Requires API key.', longDescription: 'Integrate ElevenLabs into the workflow. Can convert text to speech.',
docsLink: 'https://docs.sim.ai/tools/elevenlabs', docsLink: 'https://docs.sim.ai/tools/elevenlabs',
category: 'tools', category: 'tools',
bgColor: '#181C1E', bgColor: '#181C1E',

View File

@@ -1,13 +1,15 @@
import { ExaAIIcon } from '@/components/icons' import { ExaAIIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { ExaResponse } from '@/tools/exa/types' import type { ExaResponse } from '@/tools/exa/types'
export const ExaBlock: BlockConfig<ExaResponse> = { export const ExaBlock: BlockConfig<ExaResponse> = {
type: 'exa', type: 'exa',
name: 'Exa', name: 'Exa',
description: 'Search with Exa AI', description: 'Search with Exa AI',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Exa into the workflow. Can search, get contents, find similar links, answer a question, and perform research. Requires API Key.', 'Integrate Exa into the workflow. Can search, get contents, find similar links, answer a question, and perform research.',
docsLink: 'https://docs.sim.ai/tools/exa', docsLink: 'https://docs.sim.ai/tools/exa',
category: 'tools', category: 'tools',
bgColor: '#1F40ED', bgColor: '#1F40ED',

View File

@@ -1,13 +1,14 @@
import { FirecrawlIcon } from '@/components/icons' import { FirecrawlIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { FirecrawlResponse } from '@/tools/firecrawl/types' import type { FirecrawlResponse } from '@/tools/firecrawl/types'
export const FirecrawlBlock: BlockConfig<FirecrawlResponse> = { export const FirecrawlBlock: BlockConfig<FirecrawlResponse> = {
type: 'firecrawl', type: 'firecrawl',
name: 'Firecrawl', name: 'Firecrawl',
description: 'Scrape or search the web', description: 'Scrape or search the web',
longDescription: authMode: AuthMode.ApiKey,
'Integrate Firecrawl into the workflow. Can search, scrape, or crawl websites. Requires API Key.', longDescription: 'Integrate Firecrawl into the workflow. Can search, scrape, or crawl websites.',
docsLink: 'https://docs.sim.ai/tools/firecrawl', docsLink: 'https://docs.sim.ai/tools/firecrawl',
category: 'tools', category: 'tools',
bgColor: '#181C1E', bgColor: '#181C1E',

View File

@@ -1,6 +1,10 @@
import { WebhookIcon } from '@/components/icons' import type { SVGProps } from 'react'
import { createElement } from 'react'
import { Webhook } from 'lucide-react'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
const WebhookIcon = (props: SVGProps<SVGSVGElement>) => createElement(Webhook, props)
export const GenericWebhookBlock: BlockConfig = { export const GenericWebhookBlock: BlockConfig = {
type: 'generic_webhook', type: 'generic_webhook',
name: 'Webhook', name: 'Webhook',
@@ -8,6 +12,7 @@ export const GenericWebhookBlock: BlockConfig = {
category: 'triggers', category: 'triggers',
icon: WebhookIcon, icon: WebhookIcon,
bgColor: '#10B981', // Green color for triggers bgColor: '#10B981', // Green color for triggers
triggerAllowed: true,
subBlocks: [ subBlocks: [
// Generic webhook configuration - always visible // Generic webhook configuration - always visible

View File

@@ -1,17 +1,20 @@
import { GithubIcon } from '@/components/icons' import { GithubIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { GitHubResponse } from '@/tools/github/types' import type { GitHubResponse } from '@/tools/github/types'
export const GitHubBlock: BlockConfig<GitHubResponse> = { export const GitHubBlock: BlockConfig<GitHubResponse> = {
type: 'github', type: 'github',
name: 'GitHub', name: 'GitHub',
description: 'Interact with GitHub or trigger workflows from GitHub events', description: 'Interact with GitHub or trigger workflows from GitHub events',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Github into the workflow. Can get get PR details, create PR comment, get repository info, and get latest commit. Requires github token API Key. Can be used in trigger mode to trigger a workflow when a PR is created, commented on, or a commit is pushed.', 'Integrate Github into the workflow. Can get get PR details, create PR comment, get repository info, and get latest commit. Can be used in trigger mode to trigger a workflow when a PR is created, commented on, or a commit is pushed.',
docsLink: 'https://docs.sim.ai/tools/github', docsLink: 'https://docs.sim.ai/tools/github',
category: 'tools', category: 'tools',
bgColor: '#181C1E', bgColor: '#181C1E',
icon: GithubIcon, icon: GithubIcon,
triggerAllowed: true,
subBlocks: [ subBlocks: [
{ {
id: 'operation', id: 'operation',

View File

@@ -1,17 +1,20 @@
import { GmailIcon } from '@/components/icons' import { GmailIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { GmailToolResponse } from '@/tools/gmail/types' import type { GmailToolResponse } from '@/tools/gmail/types'
export const GmailBlock: BlockConfig<GmailToolResponse> = { export const GmailBlock: BlockConfig<GmailToolResponse> = {
type: 'gmail', type: 'gmail',
name: 'Gmail', name: 'Gmail',
description: 'Send Gmail or trigger workflows from Gmail events', description: 'Send Gmail or trigger workflows from Gmail events',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate Gmail into the workflow. Can send, read, and search emails. Requires OAuth. Can be used in trigger mode to trigger a workflow when a new email is received.', 'Integrate Gmail into the workflow. Can send, read, and search emails. Can be used in trigger mode to trigger a workflow when a new email is received.',
docsLink: 'https://docs.sim.ai/tools/gmail', docsLink: 'https://docs.sim.ai/tools/gmail',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',
icon: GmailIcon, icon: GmailIcon,
triggerAllowed: true,
subBlocks: [ subBlocks: [
// Operation selector // Operation selector
{ {

View File

@@ -1,13 +1,14 @@
import { GoogleIcon } from '@/components/icons' import { GoogleIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { GoogleSearchResponse } from '@/tools/google/types' import type { GoogleSearchResponse } from '@/tools/google/types'
export const GoogleSearchBlock: BlockConfig<GoogleSearchResponse> = { export const GoogleSearchBlock: BlockConfig<GoogleSearchResponse> = {
type: 'google_search', type: 'google_search',
name: 'Google Search', name: 'Google Search',
description: 'Search the web', description: 'Search the web',
longDescription: authMode: AuthMode.ApiKey,
'Integrate Google Search into the workflow. Can search the web. Requires API Key.', longDescription: 'Integrate Google Search into the workflow. Can search the web.',
docsLink: 'https://docs.sim.ai/tools/google_search', docsLink: 'https://docs.sim.ai/tools/google_search',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,13 +1,15 @@
import { GoogleCalendarIcon } from '@/components/icons' import { GoogleCalendarIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { GoogleCalendarResponse } from '@/tools/google_calendar/types' import type { GoogleCalendarResponse } from '@/tools/google_calendar/types'
export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = { export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
type: 'google_calendar', type: 'google_calendar',
name: 'Google Calendar', name: 'Google Calendar',
description: 'Manage Google Calendar events', description: 'Manage Google Calendar events',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate Google Calendar into the workflow. Can create, read, update, and list calendar events. Requires OAuth.', 'Integrate Google Calendar into the workflow. Can create, read, update, and list calendar events.',
docsLink: 'https://docs.sim.ai/tools/google_calendar', docsLink: 'https://docs.sim.ai/tools/google_calendar',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,13 +1,15 @@
import { GoogleDocsIcon } from '@/components/icons' import { GoogleDocsIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { GoogleDocsResponse } from '@/tools/google_docs/types' import type { GoogleDocsResponse } from '@/tools/google_docs/types'
export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = { export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
type: 'google_docs', type: 'google_docs',
name: 'Google Docs', name: 'Google Docs',
description: 'Read, write, and create documents', description: 'Read, write, and create documents',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate Google Docs into the workflow. Can read, write, and create documents. Requires OAuth.', 'Integrate Google Docs into the workflow. Can read, write, and create documents.',
docsLink: 'https://docs.sim.ai/tools/google_docs', docsLink: 'https://docs.sim.ai/tools/google_docs',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,13 +1,14 @@
import { GoogleDriveIcon } from '@/components/icons' import { GoogleDriveIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { GoogleDriveResponse } from '@/tools/google_drive/types' import type { GoogleDriveResponse } from '@/tools/google_drive/types'
export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = { export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
type: 'google_drive', type: 'google_drive',
name: 'Google Drive', name: 'Google Drive',
description: 'Create, upload, and list files', description: 'Create, upload, and list files',
longDescription: authMode: AuthMode.OAuth,
'Integrate Google Drive into the workflow. Can create, upload, and list files. Requires OAuth.', longDescription: 'Integrate Google Drive into the workflow. Can create, upload, and list files.',
docsLink: 'https://docs.sim.ai/tools/google_drive', docsLink: 'https://docs.sim.ai/tools/google_drive',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,13 +1,15 @@
import { GoogleSheetsIcon } from '@/components/icons' import { GoogleSheetsIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { GoogleSheetsResponse } from '@/tools/google_sheets/types' import type { GoogleSheetsResponse } from '@/tools/google_sheets/types'
export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = { export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
type: 'google_sheets', type: 'google_sheets',
name: 'Google Sheets', name: 'Google Sheets',
description: 'Read, write, and update data', description: 'Read, write, and update data',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate Google Sheets into the workflow. Can read, write, append, and update data. Requires OAuth.', 'Integrate Google Sheets into the workflow. Can read, write, append, and update data.',
docsLink: 'https://docs.sim.ai/tools/google_sheets', docsLink: 'https://docs.sim.ai/tools/google_sheets',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,13 +1,15 @@
import { HuggingFaceIcon } from '@/components/icons' import { HuggingFaceIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { HuggingFaceChatResponse } from '@/tools/huggingface/types' import type { HuggingFaceChatResponse } from '@/tools/huggingface/types'
export const HuggingFaceBlock: BlockConfig<HuggingFaceChatResponse> = { export const HuggingFaceBlock: BlockConfig<HuggingFaceChatResponse> = {
type: 'huggingface', type: 'huggingface',
name: 'Hugging Face', name: 'Hugging Face',
description: 'Use Hugging Face Inference API', description: 'Use Hugging Face Inference API',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Hugging Face into the workflow. Can generate completions using the Hugging Face Inference API. Requires API Key.', 'Integrate Hugging Face into the workflow. Can generate completions using the Hugging Face Inference API.',
docsLink: 'https://docs.sim.ai/tools/huggingface', docsLink: 'https://docs.sim.ai/tools/huggingface',
category: 'tools', category: 'tools',
bgColor: '#0B0F19', bgColor: '#0B0F19',

View File

@@ -1,13 +1,14 @@
import { HunterIOIcon } from '@/components/icons' import { HunterIOIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { HunterResponse } from '@/tools/hunter/types' import type { HunterResponse } from '@/tools/hunter/types'
export const HunterBlock: BlockConfig<HunterResponse> = { export const HunterBlock: BlockConfig<HunterResponse> = {
type: 'hunter', type: 'hunter',
name: 'Hunter io', name: 'Hunter io',
description: 'Find and verify professional email addresses', description: 'Find and verify professional email addresses',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Hunter into the workflow. Can search domains, find email addresses, verify email addresses, discover companies, find companies, and count email addresses. Requires API Key.', 'Integrate Hunter into the workflow. Can search domains, find email addresses, verify email addresses, discover companies, find companies, and count email addresses.',
docsLink: 'https://docs.sim.ai/tools/hunter', docsLink: 'https://docs.sim.ai/tools/hunter',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,13 +1,14 @@
import { ImageIcon } from '@/components/icons' import { ImageIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { DalleResponse } from '@/tools/openai/types' import type { DalleResponse } from '@/tools/openai/types'
export const ImageGeneratorBlock: BlockConfig<DalleResponse> = { export const ImageGeneratorBlock: BlockConfig<DalleResponse> = {
type: 'image_generator', type: 'image_generator',
name: 'Image Generator', name: 'Image Generator',
description: 'Generate images', description: 'Generate images',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Image Generator into the workflow. Can generate images using DALL-E 3 or GPT Image. Requires API Key.', 'Integrate Image Generator into the workflow. Can generate images using DALL-E 3 or GPT Image.',
docsLink: 'https://docs.sim.ai/tools/image_generator', docsLink: 'https://docs.sim.ai/tools/image_generator',
category: 'tools', category: 'tools',
bgColor: '#4D5FFF', bgColor: '#4D5FFF',

View File

@@ -0,0 +1,37 @@
import type { SVGProps } from 'react'
import { createElement } from 'react'
import { Play } from 'lucide-react'
import type { BlockConfig } from '@/blocks/types'
const InputTriggerIcon = (props: SVGProps<SVGSVGElement>) => createElement(Play, props)
export const InputTriggerBlock: BlockConfig = {
type: 'input_trigger',
name: 'Input Form',
description: 'Start workflow manually with a defined input schema',
longDescription:
'Manually trigger the workflow from the editor with a structured input schema. This enables typed inputs for parent workflows to map into.',
category: 'triggers',
bgColor: '#3B82F6',
icon: InputTriggerIcon,
subBlocks: [
{
id: 'inputFormat',
title: 'Input Format',
type: 'input-format',
layout: 'full',
description: 'Define the JSON input schema for this workflow when run manually.',
},
],
tools: {
access: [],
},
inputs: {},
outputs: {
// Dynamic outputs will be derived from inputFormat
},
triggers: {
enabled: true,
available: ['manual'],
},
}

View File

@@ -1,13 +1,13 @@
import { JinaAIIcon } from '@/components/icons' import { JinaAIIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { ReadUrlResponse } from '@/tools/jina/types' import type { ReadUrlResponse } from '@/tools/jina/types'
export const JinaBlock: BlockConfig<ReadUrlResponse> = { export const JinaBlock: BlockConfig<ReadUrlResponse> = {
type: 'jina', type: 'jina',
name: 'Jina', name: 'Jina',
description: 'Convert website content into text', description: 'Convert website content into text',
longDescription: authMode: AuthMode.ApiKey,
'Integrate Jina into the workflow. Extracts content from websites. Requires API Key.', longDescription: 'Integrate Jina into the workflow. Extracts content from websites.',
docsLink: 'https://docs.sim.ai/tools/jina', docsLink: 'https://docs.sim.ai/tools/jina',
category: 'tools', category: 'tools',
bgColor: '#333333', bgColor: '#333333',

View File

@@ -1,13 +1,14 @@
import { JiraIcon } from '@/components/icons' import { JiraIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { JiraResponse } from '@/tools/jira/types' import type { JiraResponse } from '@/tools/jira/types'
export const JiraBlock: BlockConfig<JiraResponse> = { export const JiraBlock: BlockConfig<JiraResponse> = {
type: 'jira', type: 'jira',
name: 'Jira', name: 'Jira',
description: 'Interact with Jira', description: 'Interact with Jira',
longDescription: authMode: AuthMode.OAuth,
'Integrate Jira into the workflow. Can read, write, and update issues. Requires OAuth.', longDescription: 'Integrate Jira into the workflow. Can read, write, and update issues.',
docsLink: 'https://docs.sim.ai/tools/jira', docsLink: 'https://docs.sim.ai/tools/jira',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,5 +1,6 @@
import { LinearIcon } from '@/components/icons' import { LinearIcon } from '@/components/icons'
import type { BlockConfig, BlockIcon } from '@/blocks/types' import type { BlockConfig, BlockIcon } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { LinearResponse } from '@/tools/linear/types' import type { LinearResponse } from '@/tools/linear/types'
const LinearBlockIcon: BlockIcon = (props) => LinearIcon(props as any) const LinearBlockIcon: BlockIcon = (props) => LinearIcon(props as any)
@@ -8,8 +9,8 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
type: 'linear', type: 'linear',
name: 'Linear', name: 'Linear',
description: 'Read and create issues in Linear', description: 'Read and create issues in Linear',
longDescription: authMode: AuthMode.OAuth,
'Integrate Linear into the workflow. Can read and create issues. Requires OAuth.', longDescription: 'Integrate Linear into the workflow. Can read and create issues.',
category: 'tools', category: 'tools',
icon: LinearBlockIcon, icon: LinearBlockIcon,
bgColor: '#5E6AD2', bgColor: '#5E6AD2',

View File

@@ -1,12 +1,13 @@
import { LinkupIcon } from '@/components/icons' import { LinkupIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { LinkupSearchToolResponse } from '@/tools/linkup/types' import type { LinkupSearchToolResponse } from '@/tools/linkup/types'
export const LinkupBlock: BlockConfig<LinkupSearchToolResponse> = { export const LinkupBlock: BlockConfig<LinkupSearchToolResponse> = {
type: 'linkup', type: 'linkup',
name: 'Linkup', name: 'Linkup',
description: 'Search the web with Linkup', description: 'Search the web with Linkup',
longDescription: 'Integrate Linkup into the workflow. Can search the web. Requires API Key.', authMode: AuthMode.ApiKey,
longDescription: 'Integrate Linkup into the workflow. Can search the web.',
docsLink: 'https://docs.sim.ai/tools/linkup', docsLink: 'https://docs.sim.ai/tools/linkup',
category: 'tools', category: 'tools',
bgColor: '#D6D3C7', bgColor: '#D6D3C7',

View File

@@ -1,13 +1,13 @@
import { Mem0Icon } from '@/components/icons' import { Mem0Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { Mem0Response } from '@/tools/mem0/types' import type { Mem0Response } from '@/tools/mem0/types'
export const Mem0Block: BlockConfig<Mem0Response> = { export const Mem0Block: BlockConfig<Mem0Response> = {
type: 'mem0', type: 'mem0',
name: 'Mem0', name: 'Mem0',
description: 'Agent memory management', description: 'Agent memory management',
longDescription: authMode: AuthMode.ApiKey,
'Integrate Mem0 into the workflow. Can add, search, and retrieve memories. Requires API Key.', longDescription: 'Integrate Mem0 into the workflow. Can add, search, and retrieve memories.',
bgColor: '#181C1E', bgColor: '#181C1E',
icon: Mem0Icon, icon: Mem0Icon,
category: 'tools', category: 'tools',

View File

@@ -1,13 +1,15 @@
import { MicrosoftExcelIcon } from '@/components/icons' import { MicrosoftExcelIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { MicrosoftExcelResponse } from '@/tools/microsoft_excel/types' import type { MicrosoftExcelResponse } from '@/tools/microsoft_excel/types'
export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = { export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
type: 'microsoft_excel', type: 'microsoft_excel',
name: 'Microsoft Excel', name: 'Microsoft Excel',
description: 'Read, write, and update data', description: 'Read, write, and update data',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate Microsoft Excel into the workflow. Can read, write, update, and add to table. Requires OAuth.', 'Integrate Microsoft Excel into the workflow. Can read, write, update, and add to table.',
docsLink: 'https://docs.sim.ai/tools/microsoft_excel', docsLink: 'https://docs.sim.ai/tools/microsoft_excel',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,5 +1,6 @@
import { MicrosoftPlannerIcon } from '@/components/icons' import { MicrosoftPlannerIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { MicrosoftPlannerResponse } from '@/tools/microsoft_planner/types' import type { MicrosoftPlannerResponse } from '@/tools/microsoft_planner/types'
interface MicrosoftPlannerBlockParams { interface MicrosoftPlannerBlockParams {
@@ -19,8 +20,8 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
type: 'microsoft_planner', type: 'microsoft_planner',
name: 'Microsoft Planner', name: 'Microsoft Planner',
description: 'Read and create tasks in Microsoft Planner', description: 'Read and create tasks in Microsoft Planner',
longDescription: authMode: AuthMode.OAuth,
'Integrate Microsoft Planner into the workflow. Can read and create tasks. Requires OAuth.', longDescription: 'Integrate Microsoft Planner into the workflow. Can read and create tasks.',
docsLink: 'https://docs.sim.ai/tools/microsoft_planner', docsLink: 'https://docs.sim.ai/tools/microsoft_planner',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,15 +1,18 @@
import { MicrosoftTeamsIcon } from '@/components/icons' import { MicrosoftTeamsIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { MicrosoftTeamsResponse } from '@/tools/microsoft_teams/types' import type { MicrosoftTeamsResponse } from '@/tools/microsoft_teams/types'
export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = { export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
type: 'microsoft_teams', type: 'microsoft_teams',
name: 'Microsoft Teams', name: 'Microsoft Teams',
description: 'Read, write, and create messages', description: 'Read, write, and create messages',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate Microsoft Teams into the workflow. Can read and write chat messages, and read and write channel messages. Requires OAuth. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel.', 'Integrate Microsoft Teams into the workflow. Can read and write chat messages, and read and write channel messages. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel.',
docsLink: 'https://docs.sim.ai/tools/microsoft_teams', docsLink: 'https://docs.sim.ai/tools/microsoft_teams',
category: 'tools', category: 'tools',
triggerAllowed: true,
bgColor: '#E0E0E0', bgColor: '#E0E0E0',
icon: MicrosoftTeamsIcon, icon: MicrosoftTeamsIcon,
subBlocks: [ subBlocks: [

View File

@@ -1,12 +1,13 @@
import { MistralIcon } from '@/components/icons' import { MistralIcon } from '@/components/icons'
import type { BlockConfig, SubBlockLayout, SubBlockType } from '@/blocks/types' import { AuthMode, type BlockConfig, type SubBlockLayout, type SubBlockType } from '@/blocks/types'
import type { MistralParserOutput } from '@/tools/mistral/types' import type { MistralParserOutput } from '@/tools/mistral/types'
export const MistralParseBlock: BlockConfig<MistralParserOutput> = { export const MistralParseBlock: BlockConfig<MistralParserOutput> = {
type: 'mistral_parse', type: 'mistral_parse',
name: 'Mistral Parser', name: 'Mistral Parser',
description: 'Extract text from PDF documents', description: 'Extract text from PDF documents',
longDescription: `Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL. Requires API Key.`, authMode: AuthMode.ApiKey,
longDescription: `Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL.`,
docsLink: 'https://docs.sim.ai/tools/mistral_parse', docsLink: 'https://docs.sim.ai/tools/mistral_parse',
category: 'tools', category: 'tools',
bgColor: '#000000', bgColor: '#000000',

View File

@@ -1,13 +1,15 @@
import { NotionIcon } from '@/components/icons' import { NotionIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { NotionResponse } from '@/tools/notion/types' import type { NotionResponse } from '@/tools/notion/types'
export const NotionBlock: BlockConfig<NotionResponse> = { export const NotionBlock: BlockConfig<NotionResponse> = {
type: 'notion', type: 'notion',
name: 'Notion', name: 'Notion',
description: 'Manage Notion pages', description: 'Manage Notion pages',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate with Notion into the workflow. Can read page, read database, create page, create database, append content, query database, and search workspace. Requires OAuth.', 'Integrate with Notion into the workflow. Can read page, read database, create page, create database, append content, query database, and search workspace.',
docsLink: 'https://docs.sim.ai/tools/notion', docsLink: 'https://docs.sim.ai/tools/notion',
category: 'tools', category: 'tools',
bgColor: '#181C1E', bgColor: '#181C1E',

View File

@@ -1,13 +1,14 @@
import { MicrosoftOneDriveIcon } from '@/components/icons' import { MicrosoftOneDriveIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { OneDriveResponse } from '@/tools/onedrive/types' import type { OneDriveResponse } from '@/tools/onedrive/types'
export const OneDriveBlock: BlockConfig<OneDriveResponse> = { export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'onedrive', type: 'onedrive',
name: 'OneDrive', name: 'OneDrive',
description: 'Create, upload, and list files', description: 'Create, upload, and list files',
longDescription: authMode: AuthMode.OAuth,
'Integrate OneDrive into the workflow. Can create, upload, and list files. Requires OAuth.', longDescription: 'Integrate OneDrive into the workflow. Can create, upload, and list files.',
docsLink: 'https://docs.sim.ai/tools/onedrive', docsLink: 'https://docs.sim.ai/tools/onedrive',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,12 +1,13 @@
import { OpenAIIcon } from '@/components/icons' import { OpenAIIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const OpenAIBlock: BlockConfig = { export const OpenAIBlock: BlockConfig = {
type: 'openai', type: 'openai',
name: 'Embeddings', name: 'Embeddings',
description: 'Generate Open AI embeddings', description: 'Generate Open AI embeddings',
longDescription: authMode: AuthMode.ApiKey,
'Integrate Embeddings into the workflow. Can generate embeddings from text. Requires API Key.', longDescription: 'Integrate Embeddings into the workflow. Can generate embeddings from text.',
category: 'tools', category: 'tools',
docsLink: 'https://docs.sim.ai/tools/openai', docsLink: 'https://docs.sim.ai/tools/openai',
bgColor: '#10a37f', bgColor: '#10a37f',

View File

@@ -1,15 +1,18 @@
import { OutlookIcon } from '@/components/icons' import { OutlookIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { OutlookResponse } from '@/tools/outlook/types' import type { OutlookResponse } from '@/tools/outlook/types'
export const OutlookBlock: BlockConfig<OutlookResponse> = { export const OutlookBlock: BlockConfig<OutlookResponse> = {
type: 'outlook', type: 'outlook',
name: 'Outlook', name: 'Outlook',
description: 'Access Outlook', description: 'Access Outlook',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate Outlook into the workflow. Can read, draft, and send email messages. Requires OAuth. Can be used in trigger mode to trigger a workflow when a new email is received.', 'Integrate Outlook into the workflow. Can read, draft, and send email messages. Can be used in trigger mode to trigger a workflow when a new email is received.',
docsLink: 'https://docs.sim.ai/tools/outlook', docsLink: 'https://docs.sim.ai/tools/outlook',
category: 'tools', category: 'tools',
triggerAllowed: true,
bgColor: '#E0E0E0', bgColor: '#E0E0E0',
icon: OutlookIcon, icon: OutlookIcon,
subBlocks: [ subBlocks: [

View File

@@ -1,12 +1,13 @@
import { ParallelIcon } from '@/components/icons' import { ParallelIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { ToolResponse } from '@/tools/types' import type { ToolResponse } from '@/tools/types'
export const ParallelBlock: BlockConfig<ToolResponse> = { export const ParallelBlock: BlockConfig<ToolResponse> = {
type: 'parallel_ai', type: 'parallel_ai',
name: 'Parallel AI', name: 'Parallel AI',
description: 'Search with Parallel AI', description: 'Search with Parallel AI',
longDescription: 'Integrate Parallel AI into the workflow. Can search the web. Requires API Key.', authMode: AuthMode.ApiKey,
longDescription: 'Integrate Parallel AI into the workflow. Can search the web.',
docsLink: 'https://docs.parallel.ai/search-api/search-quickstart', docsLink: 'https://docs.parallel.ai/search-api/search-quickstart',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -1,5 +1,5 @@
import { PerplexityIcon } from '@/components/icons' import { PerplexityIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { PerplexityChatResponse } from '@/tools/perplexity/types' import type { PerplexityChatResponse } from '@/tools/perplexity/types'
export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = { export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
@@ -7,7 +7,8 @@ export const PerplexityBlock: BlockConfig<PerplexityChatResponse> = {
name: 'Perplexity', name: 'Perplexity',
description: 'Use Perplexity AI chat models', description: 'Use Perplexity AI chat models',
longDescription: longDescription:
'Integrate Perplexity into the workflow. Can generate completions using Perplexity AI chat models. Requires API Key.', 'Integrate Perplexity into the workflow. Can generate completions using Perplexity AI chat models.',
authMode: AuthMode.ApiKey,
docsLink: 'https://docs.sim.ai/tools/perplexity', docsLink: 'https://docs.sim.ai/tools/perplexity',
category: 'tools', category: 'tools',
bgColor: '#20808D', // Perplexity turquoise color bgColor: '#20808D', // Perplexity turquoise color

View File

@@ -1,13 +1,15 @@
import { PineconeIcon } from '@/components/icons' import { PineconeIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { PineconeResponse } from '@/tools/pinecone/types' import type { PineconeResponse } from '@/tools/pinecone/types'
export const PineconeBlock: BlockConfig<PineconeResponse> = { export const PineconeBlock: BlockConfig<PineconeResponse> = {
type: 'pinecone', type: 'pinecone',
name: 'Pinecone', name: 'Pinecone',
description: 'Use Pinecone vector database', description: 'Use Pinecone vector database',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Pinecone into the workflow. Can generate embeddings, upsert text, search with text, fetch vectors, and search with vectors. Requires API Key.', 'Integrate Pinecone into the workflow. Can generate embeddings, upsert text, search with text, fetch vectors, and search with vectors.',
docsLink: 'https://docs.sim.ai/tools/pinecone', docsLink: 'https://docs.sim.ai/tools/pinecone',
category: 'tools', category: 'tools',
bgColor: '#0D1117', bgColor: '#0D1117',

View File

@@ -1,13 +1,14 @@
import { QdrantIcon } from '@/components/icons' import { QdrantIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { QdrantResponse } from '@/tools/qdrant/types' import type { QdrantResponse } from '@/tools/qdrant/types'
export const QdrantBlock: BlockConfig<QdrantResponse> = { export const QdrantBlock: BlockConfig<QdrantResponse> = {
type: 'qdrant', type: 'qdrant',
name: 'Qdrant', name: 'Qdrant',
description: 'Use Qdrant vector database', description: 'Use Qdrant vector database',
longDescription: authMode: AuthMode.ApiKey,
'Integrate Qdrant into the workflow. Can upsert, search, and fetch points. Requires API Key.', longDescription: 'Integrate Qdrant into the workflow. Can upsert, search, and fetch points.',
docsLink: 'https://qdrant.tech/documentation/', docsLink: 'https://qdrant.tech/documentation/',
category: 'tools', category: 'tools',
bgColor: '#1A223F', bgColor: '#1A223F',

View File

@@ -1,13 +1,15 @@
import { RedditIcon } from '@/components/icons' import { RedditIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { RedditResponse } from '@/tools/reddit/types' import type { RedditResponse } from '@/tools/reddit/types'
export const RedditBlock: BlockConfig<RedditResponse> = { export const RedditBlock: BlockConfig<RedditResponse> = {
type: 'reddit', type: 'reddit',
name: 'Reddit', name: 'Reddit',
description: 'Access Reddit data and content', description: 'Access Reddit data and content',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate Reddit into the workflow. Can get posts and comments from a subreddit. Requires OAuth.', 'Integrate Reddit into the workflow. Can get posts and comments from a subreddit.',
docsLink: 'https://docs.sim.ai/tools/reddit', docsLink: 'https://docs.sim.ai/tools/reddit',
category: 'tools', category: 'tools',
bgColor: '#FF5700', bgColor: '#FF5700',

View File

@@ -1,6 +1,6 @@
import { ConnectIcon } from '@/components/icons' import { ConnectIcon } from '@/components/icons'
import { isHosted } from '@/lib/environment' import { isHosted } from '@/lib/environment'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { ProviderId } from '@/providers/types' import type { ProviderId } from '@/providers/types'
import { import {
getAllModelProviders, getAllModelProviders,
@@ -108,6 +108,7 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
type: 'router', type: 'router',
name: 'Router', name: 'Router',
description: 'Route workflow', description: 'Route workflow',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'This is a core workflow block. Intelligently direct workflow execution to different paths based on input analysis. Use natural language to instruct the router to route to certain blocks based on the input.', 'This is a core workflow block. Intelligently direct workflow execution to different paths based on input analysis. Use natural language to instruct the router to route to certain blocks based on the input.',
category: 'blocks', category: 'blocks',

View File

@@ -1,11 +1,13 @@
import { S3Icon } from '@/components/icons' import { S3Icon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { S3Response } from '@/tools/s3/types' import type { S3Response } from '@/tools/s3/types'
export const S3Block: BlockConfig<S3Response> = { export const S3Block: BlockConfig<S3Response> = {
type: 's3', type: 's3',
name: 'S3', name: 'S3',
description: 'View S3 files', description: 'View S3 files',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate S3 into the workflow. Can get presigned URLs for S3 objects. Requires access key and secret access key.', 'Integrate S3 into the workflow. Can get presigned URLs for S3 objects. Requires access key and secret access key.',
docsLink: 'https://docs.sim.ai/tools/s3', docsLink: 'https://docs.sim.ai/tools/s3',

View File

@@ -1,6 +1,10 @@
import { ScheduleIcon } from '@/components/icons' import type { SVGProps } from 'react'
import { createElement } from 'react'
import { Clock } from 'lucide-react'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
const ScheduleIcon = (props: SVGProps<SVGSVGElement>) => createElement(Clock, props)
export const ScheduleBlock: BlockConfig = { export const ScheduleBlock: BlockConfig = {
type: 'schedule', type: 'schedule',
name: 'Schedule', name: 'Schedule',
@@ -8,7 +12,7 @@ export const ScheduleBlock: BlockConfig = {
longDescription: longDescription:
'Integrate Schedule into the workflow. Can trigger a workflow on a schedule configuration.', 'Integrate Schedule into the workflow. Can trigger a workflow on a schedule configuration.',
category: 'triggers', category: 'triggers',
bgColor: '#7B68EE', bgColor: '#6366F1',
icon: ScheduleIcon, icon: ScheduleIcon,
subBlocks: [ subBlocks: [

View File

@@ -1,12 +1,14 @@
import { SerperIcon } from '@/components/icons' import { SerperIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { SearchResponse } from '@/tools/serper/types' import type { SearchResponse } from '@/tools/serper/types'
export const SerperBlock: BlockConfig<SearchResponse> = { export const SerperBlock: BlockConfig<SearchResponse> = {
type: 'serper', type: 'serper',
name: 'Serper', name: 'Serper',
description: 'Search the web using Serper', description: 'Search the web using Serper',
longDescription: 'Integrate Serper into the workflow. Can search the web. Requires API Key.', authMode: AuthMode.ApiKey,
longDescription: 'Integrate Serper into the workflow. Can search the web.',
docsLink: 'https://docs.sim.ai/tools/serper', docsLink: 'https://docs.sim.ai/tools/serper',
category: 'tools', category: 'tools',
bgColor: '#2B3543', bgColor: '#2B3543',

View File

@@ -1,6 +1,7 @@
import { MicrosoftSharepointIcon } from '@/components/icons' import { MicrosoftSharepointIcon } from '@/components/icons'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { SharepointResponse } from '@/tools/sharepoint/types' import type { SharepointResponse } from '@/tools/sharepoint/types'
const logger = createLogger('SharepointBlock') const logger = createLogger('SharepointBlock')
@@ -9,6 +10,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
type: 'sharepoint', type: 'sharepoint',
name: 'Sharepoint', name: 'Sharepoint',
description: 'Work with pages and lists', description: 'Work with pages and lists',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate SharePoint into the workflow. Read/create pages, list sites, and work with lists (read, create, update items). Requires OAuth.', 'Integrate SharePoint into the workflow. Read/create pages, list sites, and work with lists (read, create, update items). Requires OAuth.',
docsLink: 'https://docs.sim.ai/tools/sharepoint', docsLink: 'https://docs.sim.ai/tools/sharepoint',

View File

@@ -1,17 +1,20 @@
import { SlackIcon } from '@/components/icons' import { SlackIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { SlackResponse } from '@/tools/slack/types' import type { SlackResponse } from '@/tools/slack/types'
export const SlackBlock: BlockConfig<SlackResponse> = { export const SlackBlock: BlockConfig<SlackResponse> = {
type: 'slack', type: 'slack',
name: 'Slack', name: 'Slack',
description: 'Send messages to Slack or trigger workflows from Slack events', description: 'Send messages to Slack or trigger workflows from Slack events',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate Slack into the workflow. Can send messages, create canvases, and read messages. Requires OAuth. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.', 'Integrate Slack into the workflow. Can send messages, create canvases, and read messages. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.',
docsLink: 'https://docs.sim.ai/tools/slack', docsLink: 'https://docs.sim.ai/tools/slack',
category: 'tools', category: 'tools',
bgColor: '#611f69', bgColor: '#611f69',
icon: SlackIcon, icon: SlackIcon,
triggerAllowed: true,
subBlocks: [ subBlocks: [
{ {
id: 'operation', id: 'operation',

View File

@@ -1,5 +1,5 @@
import { StagehandIcon } from '@/components/icons' import { StagehandIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { ToolResponse } from '@/tools/types' import type { ToolResponse } from '@/tools/types'
export interface StagehandExtractResponse extends ToolResponse { export interface StagehandExtractResponse extends ToolResponse {
@@ -12,8 +12,9 @@ export const StagehandBlock: BlockConfig<StagehandExtractResponse> = {
type: 'stagehand', type: 'stagehand',
name: 'Stagehand Extract', name: 'Stagehand Extract',
description: 'Extract data from websites', description: 'Extract data from websites',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Stagehand into the workflow. Can extract structured data from webpages. Requires API Key.', 'Integrate Stagehand into the workflow. Can extract structured data from webpages.',
docsLink: 'https://docs.sim.ai/tools/stagehand', docsLink: 'https://docs.sim.ai/tools/stagehand',
category: 'tools', category: 'tools',
bgColor: '#FFC83C', bgColor: '#FFC83C',

View File

@@ -1,13 +1,14 @@
import { StagehandIcon } from '@/components/icons' import { StagehandIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { StagehandAgentResponse } from '@/tools/stagehand/types' import type { StagehandAgentResponse } from '@/tools/stagehand/types'
export const StagehandAgentBlock: BlockConfig<StagehandAgentResponse> = { export const StagehandAgentBlock: BlockConfig<StagehandAgentResponse> = {
type: 'stagehand_agent', type: 'stagehand_agent',
name: 'Stagehand Agent', name: 'Stagehand Agent',
description: 'Autonomous web browsing agent', description: 'Autonomous web browsing agent',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Stagehand Agent into the workflow. Can navigate the web and perform tasks. Requires API Key.', 'Integrate Stagehand Agent into the workflow. Can navigate the web and perform tasks.',
docsLink: 'https://docs.sim.ai/tools/stagehand_agent', docsLink: 'https://docs.sim.ai/tools/stagehand_agent',
category: 'tools', category: 'tools',
bgColor: '#FFC83C', bgColor: '#FFC83C',

View File

@@ -9,6 +9,7 @@ export const StarterBlock: BlockConfig = {
category: 'blocks', category: 'blocks',
bgColor: '#2FB3FF', bgColor: '#2FB3FF',
icon: StartIcon, icon: StartIcon,
hideFromToolbar: true,
subBlocks: [ subBlocks: [
// Main trigger selector // Main trigger selector
{ {

View File

@@ -1,6 +1,6 @@
import { SupabaseIcon } from '@/components/icons' import { SupabaseIcon } from '@/components/icons'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import type { SupabaseResponse } from '@/tools/supabase/types' import type { SupabaseResponse } from '@/tools/supabase/types'
const logger = createLogger('SupabaseBlock') const logger = createLogger('SupabaseBlock')
@@ -9,6 +9,7 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
type: 'supabase', type: 'supabase',
name: 'Supabase', name: 'Supabase',
description: 'Use Supabase database', description: 'Use Supabase database',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Supabase into the workflow. Can get many rows, get, create, update, delete, and upsert a row.', 'Integrate Supabase into the workflow. Can get many rows, get, create, update, delete, and upsert a row.',
docsLink: 'https://docs.sim.ai/tools/supabase', docsLink: 'https://docs.sim.ai/tools/supabase',

View File

@@ -1,11 +1,13 @@
import { TavilyIcon } from '@/components/icons' import { TavilyIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { TavilyResponse } from '@/tools/tavily/types' import type { TavilyResponse } from '@/tools/tavily/types'
export const TavilyBlock: BlockConfig<TavilyResponse> = { export const TavilyBlock: BlockConfig<TavilyResponse> = {
type: 'tavily', type: 'tavily',
name: 'Tavily', name: 'Tavily',
description: 'Search and extract information', description: 'Search and extract information',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Tavily into the workflow. Can search the web and extract content from specific URLs. Requires API Key.', 'Integrate Tavily into the workflow. Can search the web and extract content from specific URLs. Requires API Key.',
category: 'tools', category: 'tools',

View File

@@ -1,17 +1,20 @@
import { TelegramIcon } from '@/components/icons' import { TelegramIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { TelegramMessageResponse } from '@/tools/telegram/types' import type { TelegramMessageResponse } from '@/tools/telegram/types'
export const TelegramBlock: BlockConfig<TelegramMessageResponse> = { export const TelegramBlock: BlockConfig<TelegramMessageResponse> = {
type: 'telegram', type: 'telegram',
name: 'Telegram', name: 'Telegram',
description: 'Send messages through Telegram or trigger workflows from Telegram events', description: 'Send messages through Telegram or trigger workflows from Telegram events',
authMode: AuthMode.BotToken,
longDescription: longDescription:
'Integrate Telegram into the workflow. Can send messages. Can be used in trigger mode to trigger a workflow when a message is sent to a chat.', 'Integrate Telegram into the workflow. Can send messages. Can be used in trigger mode to trigger a workflow when a message is sent to a chat.',
docsLink: 'https://docs.sim.ai/tools/telegram', docsLink: 'https://docs.sim.ai/tools/telegram',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',
icon: TelegramIcon, icon: TelegramIcon,
triggerAllowed: true,
subBlocks: [ subBlocks: [
{ {
id: 'botToken', id: 'botToken',

View File

@@ -1,6 +1,6 @@
import { TranslateIcon } from '@/components/icons' import { TranslateIcon } from '@/components/icons'
import { isHosted } from '@/lib/environment' import { isHosted } from '@/lib/environment'
import type { BlockConfig } from '@/blocks/types' import { AuthMode, type BlockConfig } from '@/blocks/types'
import { import {
getAllModelProviders, getAllModelProviders,
getBaseModelProviders, getBaseModelProviders,
@@ -29,6 +29,7 @@ export const TranslateBlock: BlockConfig = {
type: 'translate', type: 'translate',
name: 'Translate', name: 'Translate',
description: 'Translate text to any language', description: 'Translate text to any language',
authMode: AuthMode.ApiKey,
longDescription: 'Integrate Translate into the workflow. Can translate text to any language.', longDescription: 'Integrate Translate into the workflow. Can translate text to any language.',
docsLink: 'https://docs.sim.ai/tools/translate', docsLink: 'https://docs.sim.ai/tools/translate',
category: 'tools', category: 'tools',

View File

@@ -1,11 +1,13 @@
import { TwilioIcon } from '@/components/icons' import { TwilioIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { TwilioSMSBlockOutput } from '@/tools/twilio/types' import type { TwilioSMSBlockOutput } from '@/tools/twilio/types'
export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = { export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
type: 'twilio_sms', type: 'twilio_sms',
name: 'Twilio SMS', name: 'Twilio SMS',
description: 'Send SMS messages', description: 'Send SMS messages',
authMode: AuthMode.ApiKey,
longDescription: 'Integrate Twilio into the workflow. Can send SMS messages.', longDescription: 'Integrate Twilio into the workflow. Can send SMS messages.',
category: 'tools', category: 'tools',
bgColor: '#F22F46', // Twilio brand color bgColor: '#F22F46', // Twilio brand color

View File

@@ -1,11 +1,13 @@
import { TypeformIcon } from '@/components/icons' import { TypeformIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { TypeformResponse } from '@/tools/typeform/types' import type { TypeformResponse } from '@/tools/typeform/types'
export const TypeformBlock: BlockConfig<TypeformResponse> = { export const TypeformBlock: BlockConfig<TypeformResponse> = {
type: 'typeform', type: 'typeform',
name: 'Typeform', name: 'Typeform',
description: 'Interact with Typeform', description: 'Interact with Typeform',
authMode: AuthMode.ApiKey,
longDescription: longDescription:
'Integrate Typeform into the workflow. Can retrieve responses, download files, and get form insights. Requires API Key.', 'Integrate Typeform into the workflow. Can retrieve responses, download files, and get form insights. Requires API Key.',
docsLink: 'https://docs.sim.ai/tools/typeform', docsLink: 'https://docs.sim.ai/tools/typeform',

View File

@@ -1,13 +1,14 @@
import { EyeIcon } from '@/components/icons' import { EyeIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { VisionResponse } from '@/tools/vision/types' import type { VisionResponse } from '@/tools/vision/types'
export const VisionBlock: BlockConfig<VisionResponse> = { export const VisionBlock: BlockConfig<VisionResponse> = {
type: 'vision', type: 'vision',
name: 'Vision', name: 'Vision',
description: 'Analyze images with vision models', description: 'Analyze images with vision models',
longDescription: authMode: AuthMode.ApiKey,
'Integrate Vision into the workflow. Can analyze images with vision models. Requires API Key.', longDescription: 'Integrate Vision into the workflow. Can analyze images with vision models.',
docsLink: 'https://docs.sim.ai/tools/vision', docsLink: 'https://docs.sim.ai/tools/vision',
category: 'tools', category: 'tools',
bgColor: '#4D5FFF', bgColor: '#4D5FFF',

View File

@@ -1,13 +1,15 @@
import { WealthboxIcon } from '@/components/icons' import { WealthboxIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { WealthboxResponse } from '@/tools/wealthbox/types' import type { WealthboxResponse } from '@/tools/wealthbox/types'
export const WealthboxBlock: BlockConfig<WealthboxResponse> = { export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
type: 'wealthbox', type: 'wealthbox',
name: 'Wealthbox', name: 'Wealthbox',
description: 'Interact with Wealthbox', description: 'Interact with Wealthbox',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate Wealthbox into the workflow. Can read and write notes, read and write contacts, and read and write tasks. Requires OAuth.', 'Integrate Wealthbox into the workflow. Can read and write notes, read and write contacts, and read and write tasks.',
docsLink: 'https://docs.sim.ai/tools/wealthbox', docsLink: 'https://docs.sim.ai/tools/wealthbox',
category: 'tools', category: 'tools',
bgColor: '#E0E0E0', bgColor: '#E0E0E0',

View File

@@ -13,6 +13,7 @@ import {
WhatsAppIcon, WhatsAppIcon,
} from '@/components/icons' } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
const getWebhookProviderIcon = (provider: string) => { const getWebhookProviderIcon = (provider: string) => {
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = { const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
@@ -36,9 +37,11 @@ export const WebhookBlock: BlockConfig = {
type: 'webhook', type: 'webhook',
name: 'Webhook', name: 'Webhook',
description: 'Trigger workflow execution from external webhooks', description: 'Trigger workflow execution from external webhooks',
authMode: AuthMode.OAuth,
category: 'triggers', category: 'triggers',
icon: WebhookIcon, icon: WebhookIcon,
bgColor: '#10B981', // Green color for triggers bgColor: '#10B981', // Green color for triggers
triggerAllowed: true,
hideFromToolbar: true, // Hidden for backwards compatibility - use generic webhook trigger instead hideFromToolbar: true, // Hidden for backwards compatibility - use generic webhook trigger instead
subBlocks: [ subBlocks: [

View File

@@ -1,16 +1,19 @@
import { WhatsAppIcon } from '@/components/icons' import { WhatsAppIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { WhatsAppResponse } from '@/tools/whatsapp/types' import type { WhatsAppResponse } from '@/tools/whatsapp/types'
export const WhatsAppBlock: BlockConfig<WhatsAppResponse> = { export const WhatsAppBlock: BlockConfig<WhatsAppResponse> = {
type: 'whatsapp', type: 'whatsapp',
name: 'WhatsApp', name: 'WhatsApp',
description: 'Send WhatsApp messages', description: 'Send WhatsApp messages',
authMode: AuthMode.ApiKey,
longDescription: 'Integrate WhatsApp into the workflow. Can send messages.', longDescription: 'Integrate WhatsApp into the workflow. Can send messages.',
docsLink: 'https://docs.sim.ai/tools/whatsapp', docsLink: 'https://docs.sim.ai/tools/whatsapp',
category: 'tools', category: 'tools',
bgColor: '#25D366', bgColor: '#25D366',
icon: WhatsAppIcon, icon: WhatsAppIcon,
triggerAllowed: true,
subBlocks: [ subBlocks: [
{ {
id: 'phoneNumber', id: 'phoneNumber',

View File

@@ -80,4 +80,5 @@ export const WorkflowBlock: BlockConfig = {
result: { type: 'json', description: 'Workflow execution result' }, result: { type: 'json', description: 'Workflow execution result' },
error: { type: 'string', description: 'Error message' }, error: { type: 'string', description: 'Error message' },
}, },
hideFromToolbar: true,
} }

View File

@@ -0,0 +1,58 @@
import { WorkflowIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
// Helper: list workflows excluding self
const getAvailableWorkflows = (): Array<{ label: string; id: string }> => {
try {
const { workflows, activeWorkflowId } = useWorkflowRegistry.getState()
return Object.entries(workflows)
.filter(([id]) => id !== activeWorkflowId)
.map(([id, w]) => ({ label: w.name || `Workflow ${id.slice(0, 8)}`, id }))
.sort((a, b) => a.label.localeCompare(b.label))
} catch {
return []
}
}
// New workflow block variant that visualizes child Input Trigger schema for mapping
export const WorkflowInputBlock: BlockConfig = {
type: 'workflow_input',
name: 'Workflow',
description: 'Execute another workflow and map variables to its Input Trigger schema.',
category: 'blocks',
bgColor: '#6366F1', // Indigo - modern and professional
icon: WorkflowIcon,
subBlocks: [
{
id: 'workflowId',
title: 'Select Workflow',
type: 'dropdown',
options: getAvailableWorkflows,
required: true,
},
// Renders dynamic mapping UI based on selected child workflow's Input Trigger inputFormat
{
id: 'inputMapping',
title: 'Input Mapping',
type: 'input-mapping',
layout: 'full',
description:
"Map fields defined in the child workflow's Input Trigger to variables/values in this workflow.",
dependsOn: ['workflowId'],
},
],
tools: {
access: ['workflow_executor'],
},
inputs: {
workflowId: { type: 'string', description: 'ID of the child workflow' },
inputMapping: { type: 'json', description: 'Mapping of input fields to values' },
},
outputs: {
success: { type: 'boolean', description: 'Execution success status' },
childWorkflowName: { type: 'string', description: 'Child workflow name' },
result: { type: 'json', description: 'Workflow execution result' },
error: { type: 'string', description: 'Error message' },
},
}

View File

@@ -1,13 +1,15 @@
import { xIcon } from '@/components/icons' import { xIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { XResponse } from '@/tools/x/types' import type { XResponse } from '@/tools/x/types'
export const XBlock: BlockConfig<XResponse> = { export const XBlock: BlockConfig<XResponse> = {
type: 'x', type: 'x',
name: 'X', name: 'X',
description: 'Interact with X', description: 'Interact with X',
authMode: AuthMode.OAuth,
longDescription: longDescription:
'Integrate X into the workflow. Can post a new tweet, get tweet details, search tweets, and get user profile. Requires OAuth.', 'Integrate X into the workflow. Can post a new tweet, get tweet details, search tweets, and get user profile.',
docsLink: 'https://docs.sim.ai/tools/x', docsLink: 'https://docs.sim.ai/tools/x',
category: 'tools', category: 'tools',
bgColor: '#000000', // X's black color bgColor: '#000000', // X's black color

View File

@@ -1,16 +1,20 @@
import { YouTubeIcon } from '@/components/icons' import { YouTubeIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig, BlockIcon } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { YouTubeSearchResponse } from '@/tools/youtube/types' import type { YouTubeSearchResponse } from '@/tools/youtube/types'
const YouTubeBlockIcon: BlockIcon = (props) => YouTubeIcon(props as any)
export const YouTubeBlock: BlockConfig<YouTubeSearchResponse> = { export const YouTubeBlock: BlockConfig<YouTubeSearchResponse> = {
type: 'youtube', type: 'youtube',
name: 'YouTube', name: 'YouTube',
description: 'Search for videos on YouTube', description: 'Search for videos on YouTube',
longDescription: 'Integrate YouTube into the workflow. Can search for videos. Requires API Key.', authMode: AuthMode.ApiKey,
longDescription: 'Integrate YouTube into the workflow. Can search for videos.',
docsLink: 'https://docs.sim.ai/tools/youtube', docsLink: 'https://docs.sim.ai/tools/youtube',
category: 'tools', category: 'tools',
bgColor: '#FF0000', bgColor: '#FF0000',
icon: YouTubeIcon, icon: YouTubeBlockIcon,
subBlocks: [ subBlocks: [
{ {
id: 'query', id: 'query',

View File

@@ -6,8 +6,10 @@
import { AgentBlock } from '@/blocks/blocks/agent' import { AgentBlock } from '@/blocks/blocks/agent'
import { AirtableBlock } from '@/blocks/blocks/airtable' import { AirtableBlock } from '@/blocks/blocks/airtable'
import { ApiBlock } from '@/blocks/blocks/api' import { ApiBlock } from '@/blocks/blocks/api'
import { ApiTriggerBlock } from '@/blocks/blocks/api_trigger'
import { ArxivBlock } from '@/blocks/blocks/arxiv' import { ArxivBlock } from '@/blocks/blocks/arxiv'
import { BrowserUseBlock } from '@/blocks/blocks/browser_use' import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
import { ClayBlock } from '@/blocks/blocks/clay' import { ClayBlock } from '@/blocks/blocks/clay'
import { ConditionBlock } from '@/blocks/blocks/condition' import { ConditionBlock } from '@/blocks/blocks/condition'
import { ConfluenceBlock } from '@/blocks/blocks/confluence' import { ConfluenceBlock } from '@/blocks/blocks/confluence'
@@ -30,6 +32,7 @@ import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets'
import { HuggingFaceBlock } from '@/blocks/blocks/huggingface' import { HuggingFaceBlock } from '@/blocks/blocks/huggingface'
import { HunterBlock } from '@/blocks/blocks/hunter' import { HunterBlock } from '@/blocks/blocks/hunter'
import { ImageGeneratorBlock } from '@/blocks/blocks/image_generator' import { ImageGeneratorBlock } from '@/blocks/blocks/image_generator'
import { InputTriggerBlock } from '@/blocks/blocks/input_trigger'
import { JinaBlock } from '@/blocks/blocks/jina' import { JinaBlock } from '@/blocks/blocks/jina'
import { JiraBlock } from '@/blocks/blocks/jira' import { JiraBlock } from '@/blocks/blocks/jira'
import { KnowledgeBlock } from '@/blocks/blocks/knowledge' import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
@@ -78,6 +81,7 @@ import { WebhookBlock } from '@/blocks/blocks/webhook'
import { WhatsAppBlock } from '@/blocks/blocks/whatsapp' import { WhatsAppBlock } from '@/blocks/blocks/whatsapp'
import { WikipediaBlock } from '@/blocks/blocks/wikipedia' import { WikipediaBlock } from '@/blocks/blocks/wikipedia'
import { WorkflowBlock } from '@/blocks/blocks/workflow' import { WorkflowBlock } from '@/blocks/blocks/workflow'
import { WorkflowInputBlock } from '@/blocks/blocks/workflow_input'
import { XBlock } from '@/blocks/blocks/x' import { XBlock } from '@/blocks/blocks/x'
import { YouTubeBlock } from '@/blocks/blocks/youtube' import { YouTubeBlock } from '@/blocks/blocks/youtube'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
@@ -147,6 +151,9 @@ export const registry: Record<string, BlockConfig> = {
stagehand_agent: StagehandAgentBlock, stagehand_agent: StagehandAgentBlock,
slack: SlackBlock, slack: SlackBlock,
starter: StarterBlock, starter: StarterBlock,
input_trigger: InputTriggerBlock,
chat_trigger: ChatTriggerBlock,
api_trigger: ApiTriggerBlock,
supabase: SupabaseBlock, supabase: SupabaseBlock,
tavily: TavilyBlock, tavily: TavilyBlock,
telegram: TelegramBlock, telegram: TelegramBlock,
@@ -160,6 +167,7 @@ export const registry: Record<string, BlockConfig> = {
whatsapp: WhatsAppBlock, whatsapp: WhatsAppBlock,
wikipedia: WikipediaBlock, wikipedia: WikipediaBlock,
workflow: WorkflowBlock, workflow: WorkflowBlock,
workflow_input: WorkflowInputBlock,
x: XBlock, x: XBlock,
youtube: YouTubeBlock, youtube: YouTubeBlock,
} }

View File

@@ -7,6 +7,13 @@ export type PrimitiveValueType = 'string' | 'number' | 'boolean' | 'json' | 'arr
export type BlockCategory = 'blocks' | 'tools' | 'triggers' export type BlockCategory = 'blocks' | 'tools' | 'triggers'
// Authentication modes for sub-blocks and summaries
export enum AuthMode {
OAuth = 'oauth',
ApiKey = 'api_key',
BotToken = 'bot_token',
}
export type GenerationType = export type GenerationType =
| 'javascript-function-body' | 'javascript-function-body'
| 'typescript-function-body' | 'typescript-function-body'
@@ -54,6 +61,7 @@ export type SubBlockType =
| 'input-format' // Input structure format | 'input-format' // Input structure format
| 'response-format' // Response structure format | 'response-format' // Response structure format
| 'file-upload' // File uploader | 'file-upload' // File uploader
| 'input-mapping' // Map parent variables to child workflow input schema
export type SubBlockLayout = 'full' | 'half' export type SubBlockLayout = 'full' | 'half'
@@ -186,6 +194,8 @@ export interface BlockConfig<T extends ToolResponse = ToolResponse> {
bgColor: string bgColor: string
icon: BlockIcon icon: BlockIcon
subBlocks: SubBlockConfig[] subBlocks: SubBlockConfig[]
triggerAllowed?: boolean
authMode?: AuthMode
tools: { tools: {
access: string[] access: string[]
config?: { config?: {

View File

@@ -4,6 +4,7 @@ import { ChevronRight } from 'lucide-react'
import { BlockPathCalculator } from '@/lib/block-path-calculator' import { BlockPathCalculator } from '@/lib/block-path-calculator'
import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format' import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { getBlockOutputPaths, getBlockOutputType } from '@/lib/workflows/block-outputs'
import { getBlock } from '@/blocks' import { getBlock } from '@/blocks'
import type { BlockConfig } from '@/blocks/types' import type { BlockConfig } from '@/blocks/types'
import { Serializer } from '@/serializer' import { Serializer } from '@/serializer'
@@ -101,7 +102,8 @@ const getOutputTypeForPath = (
block: BlockState, block: BlockState,
blockConfig: BlockConfig | null, blockConfig: BlockConfig | null,
blockId: string, blockId: string,
outputPath: string outputPath: string,
mergedSubBlocksOverride?: Record<string, any>
): string => { ): string => {
if (block?.triggerMode && blockConfig?.triggers?.enabled) { if (block?.triggerMode && blockConfig?.triggers?.enabled) {
const triggerId = blockConfig?.triggers?.available?.[0] const triggerId = blockConfig?.triggers?.available?.[0]
@@ -125,7 +127,8 @@ const getOutputTypeForPath = (
} }
} else if (block?.type === 'starter') { } else if (block?.type === 'starter') {
// Handle starter block specific outputs // Handle starter block specific outputs
const startWorkflowValue = getSubBlockValue(blockId, 'startWorkflow') const startWorkflowValue =
mergedSubBlocksOverride?.startWorkflow?.value ?? getSubBlockValue(blockId, 'startWorkflow')
if (startWorkflowValue === 'chat') { if (startWorkflowValue === 'chat') {
// Define types for chat mode outputs // Define types for chat mode outputs
@@ -137,7 +140,8 @@ const getOutputTypeForPath = (
return chatModeTypes[outputPath] || 'any' return chatModeTypes[outputPath] || 'any'
} }
// For API mode, check inputFormat for custom field types // For API mode, check inputFormat for custom field types
const inputFormatValue = getSubBlockValue(blockId, 'inputFormat') const inputFormatValue =
mergedSubBlocksOverride?.inputFormat?.value ?? getSubBlockValue(blockId, 'inputFormat')
if (inputFormatValue && Array.isArray(inputFormatValue)) { if (inputFormatValue && Array.isArray(inputFormatValue)) {
const field = inputFormatValue.find( const field = inputFormatValue.find(
(f: { name?: string; type?: string }) => f.name === outputPath (f: { name?: string; type?: string }) => f.name === outputPath
@@ -146,6 +150,11 @@ const getOutputTypeForPath = (
return field.type return field.type
} }
} }
} else if (blockConfig?.category === 'triggers') {
// For trigger blocks, use the dynamic output helper
const blockState = useWorkflowStore.getState().blocks[blockId]
const subBlocks = mergedSubBlocksOverride ?? (blockState?.subBlocks || {})
return getBlockOutputType(block.type, outputPath, subBlocks)
} else { } else {
const operationValue = getSubBlockValue(blockId, 'operation') const operationValue = getSubBlockValue(blockId, 'operation')
if (blockConfig && operationValue) { if (blockConfig && operationValue) {
@@ -297,6 +306,24 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
const edges = useWorkflowStore((state) => state.edges) const edges = useWorkflowStore((state) => state.edges)
const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId) const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
// Subscribe to live subblock values for the active workflow to react to input format changes
const workflowSubBlockValues = useSubBlockStore((state) =>
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
)
const getMergedSubBlocks = useCallback(
(targetBlockId: string): Record<string, any> => {
const base = blocks[targetBlockId]?.subBlocks || {}
const live = workflowSubBlockValues?.[targetBlockId] || {}
const merged: Record<string, any> = { ...base }
for (const [subId, liveVal] of Object.entries(live)) {
merged[subId] = { ...(base[subId] || {}), value: liveVal }
}
return merged
},
[blocks, workflowSubBlockValues]
)
const getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId) const getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId)
const variables = useVariablesStore((state) => state.variables) const variables = useVariablesStore((state) => state.variables)
const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : [] const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : []
@@ -355,7 +382,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
const blockName = sourceBlock.name || sourceBlock.type const blockName = sourceBlock.name || sourceBlock.type
const normalizedBlockName = normalizeBlockName(blockName) const normalizedBlockName = normalizeBlockName(blockName)
const responseFormatValue = getSubBlockValue(activeSourceBlockId, 'responseFormat') const mergedSubBlocks = getMergedSubBlocks(activeSourceBlockId)
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
const responseFormat = parseResponseFormatSafely(responseFormatValue, activeSourceBlockId) const responseFormat = parseResponseFormatSafely(responseFormatValue, activeSourceBlockId)
let blockTags: string[] let blockTags: string[]
@@ -382,7 +410,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
} }
} else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) { } else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) {
if (sourceBlock.type === 'starter') { if (sourceBlock.type === 'starter') {
const startWorkflowValue = getSubBlockValue(activeSourceBlockId, 'startWorkflow') const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value
if (startWorkflowValue === 'chat') { if (startWorkflowValue === 'chat') {
// For chat mode, provide input, conversationId, and files // For chat mode, provide input, conversationId, and files
@@ -392,7 +420,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
`${normalizedBlockName}.files`, `${normalizedBlockName}.files`,
] ]
} else { } else {
const inputFormatValue = getSubBlockValue(activeSourceBlockId, 'inputFormat') const inputFormatValue = mergedSubBlocks?.inputFormat?.value
if ( if (
inputFormatValue && inputFormatValue &&
@@ -410,7 +438,17 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
blockTags = [normalizedBlockName] blockTags = [normalizedBlockName]
} }
} else { } else {
if (sourceBlock?.triggerMode && blockConfig.triggers?.enabled) { // For triggers and starter blocks, use dynamic outputs based on live subblock values
if (blockConfig.category === 'triggers' || sourceBlock.type === 'starter') {
const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
if (dynamicOutputs.length > 0) {
blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
} else if (sourceBlock.type === 'starter') {
blockTags = [normalizedBlockName]
} else {
blockTags = []
}
} else if (sourceBlock?.triggerMode && blockConfig.triggers?.enabled) {
const triggerId = blockConfig?.triggers?.available?.[0] const triggerId = blockConfig?.triggers?.available?.[0]
const firstTrigger = triggerId const firstTrigger = triggerId
? getTrigger(triggerId) ? getTrigger(triggerId)
@@ -426,7 +464,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
} }
} else { } else {
// Check for tool-specific outputs first // Check for tool-specific outputs first
const operationValue = getSubBlockValue(activeSourceBlockId, 'operation') const operationValue =
mergedSubBlocks?.operation?.value ?? getSubBlockValue(activeSourceBlockId, 'operation')
const toolOutputPaths = operationValue const toolOutputPaths = operationValue
? generateToolOutputPaths(blockConfig, operationValue) ? generateToolOutputPaths(blockConfig, operationValue)
: [] : []
@@ -625,12 +664,34 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
const blockName = accessibleBlock.name || accessibleBlock.type const blockName = accessibleBlock.name || accessibleBlock.type
const normalizedBlockName = normalizeBlockName(blockName) const normalizedBlockName = normalizeBlockName(blockName)
const responseFormatValue = getSubBlockValue(accessibleBlockId, 'responseFormat') const mergedSubBlocks = getMergedSubBlocks(accessibleBlockId)
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
const responseFormat = parseResponseFormatSafely(responseFormatValue, accessibleBlockId) const responseFormat = parseResponseFormatSafely(responseFormatValue, accessibleBlockId)
let blockTags: string[] let blockTags: string[]
if (accessibleBlock.type === 'evaluator') { // For trigger blocks, use the dynamic output helper
if (blockConfig.category === 'triggers' || accessibleBlock.type === 'starter') {
const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
if (dynamicOutputs.length > 0) {
blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
} else if (accessibleBlock.type === 'starter') {
// Legacy starter block fallback
const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value
if (startWorkflowValue === 'chat') {
blockTags = [
`${normalizedBlockName}.input`,
`${normalizedBlockName}.conversationId`,
`${normalizedBlockName}.files`,
]
} else {
blockTags = [normalizedBlockName]
}
} else {
blockTags = []
}
} else if (accessibleBlock.type === 'evaluator') {
const metricsValue = getSubBlockValue(accessibleBlockId, 'metrics') const metricsValue = getSubBlockValue(accessibleBlockId, 'metrics')
if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) {
@@ -651,34 +712,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
} }
} else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) { } else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) {
if (accessibleBlock.type === 'starter') { blockTags = [normalizedBlockName]
const startWorkflowValue = getSubBlockValue(accessibleBlockId, 'startWorkflow')
if (startWorkflowValue === 'chat') {
// For chat mode, provide input, conversationId, and files
blockTags = [
`${normalizedBlockName}.input`,
`${normalizedBlockName}.conversationId`,
`${normalizedBlockName}.files`,
]
} else {
const inputFormatValue = getSubBlockValue(accessibleBlockId, 'inputFormat')
if (
inputFormatValue &&
Array.isArray(inputFormatValue) &&
inputFormatValue.length > 0
) {
blockTags = inputFormatValue
.filter((field: { name?: string }) => field.name && field.name.trim() !== '')
.map((field: { name: string }) => `${normalizedBlockName}.${field.name}`)
} else {
blockTags = [normalizedBlockName]
}
}
} else {
blockTags = [normalizedBlockName]
}
} else { } else {
const blockState = blocks[accessibleBlockId] const blockState = blocks[accessibleBlockId]
if (blockState?.triggerMode && blockConfig.triggers?.enabled) { if (blockState?.triggerMode && blockConfig.triggers?.enabled) {
@@ -697,7 +731,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
} }
} else { } else {
// Check for tool-specific outputs first // Check for tool-specific outputs first
const operationValue = getSubBlockValue(accessibleBlockId, 'operation') const operationValue =
mergedSubBlocks?.operation?.value ?? getSubBlockValue(accessibleBlockId, 'operation')
const toolOutputPaths = operationValue const toolOutputPaths = operationValue
? generateToolOutputPaths(blockConfig, operationValue) ? generateToolOutputPaths(blockConfig, operationValue)
: [] : []
@@ -746,7 +781,17 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
variableInfoMap, variableInfoMap,
blockTagGroups: finalBlockTagGroups, blockTagGroups: finalBlockTagGroups,
} }
}, [blocks, edges, loops, parallels, blockId, activeSourceBlockId, workflowVariables]) }, [
blocks,
edges,
loops,
parallels,
blockId,
activeSourceBlockId,
workflowVariables,
workflowSubBlockValues,
getMergedSubBlocks,
])
const filteredTags = useMemo(() => { const filteredTags = useMemo(() => {
if (!searchTerm) return tags if (!searchTerm) return tags
@@ -1328,12 +1373,14 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
) )
if (block) { if (block) {
const blockConfig = getBlock(block.type) const blockConfig = getBlock(block.type)
const mergedSubBlocks = getMergedSubBlocks(group.blockId)
tagDescription = getOutputTypeForPath( tagDescription = getOutputTypeForPath(
block, block,
blockConfig || null, blockConfig || null,
group.blockId, group.blockId,
outputPath outputPath,
mergedSubBlocks
) )
} }
} }
@@ -1468,12 +1515,14 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
) )
if (block) { if (block) {
const blockConfig = getBlock(block.type) const blockConfig = getBlock(block.type)
const mergedSubBlocks = getMergedSubBlocks(group.blockId)
childType = getOutputTypeForPath( childType = getOutputTypeForPath(
block, block,
blockConfig || null, blockConfig || null,
group.blockId, group.blockId,
childOutputPath childOutputPath,
mergedSubBlocks
) )
} }

View File

@@ -22,8 +22,20 @@ const MAX_WORKFLOW_DEPTH = 10
export class WorkflowBlockHandler implements BlockHandler { export class WorkflowBlockHandler implements BlockHandler {
private serializer = new Serializer() private serializer = new Serializer()
// Tolerant JSON parser for mapping values
// Keeps handler self-contained without introducing utilities
private safeParse(input: unknown): unknown {
if (typeof input !== 'string') return input
try {
return JSON.parse(input)
} catch {
return input
}
}
canHandle(block: SerializedBlock): boolean { canHandle(block: SerializedBlock): boolean {
return block.metadata?.id === BlockType.WORKFLOW const id = block.metadata?.id
return id === BlockType.WORKFLOW || id === 'workflow_input'
} }
async execute( async execute(
@@ -63,13 +75,22 @@ export class WorkflowBlockHandler implements BlockHandler {
) )
// Prepare the input for the child workflow // Prepare the input for the child workflow
// The input from this block should be passed as start.input to the child workflow // Prefer structured mapping if provided; otherwise fall back to legacy 'input' passthrough
let childWorkflowInput = {} let childWorkflowInput: Record<string, any> = {}
if (inputs.input !== undefined) { if (inputs.inputMapping !== undefined && inputs.inputMapping !== null) {
// If input is provided, use it directly // Handle inputMapping - could be object or stringified JSON
const raw = inputs.inputMapping
const normalized = this.safeParse(raw)
if (normalized && typeof normalized === 'object' && !Array.isArray(normalized)) {
childWorkflowInput = normalized as Record<string, any>
} else {
childWorkflowInput = {}
}
} else if (inputs.input !== undefined) {
// Legacy behavior: pass under start.input
childWorkflowInput = inputs.input childWorkflowInput = inputs.input
logger.info(`Passing input to child workflow: ${JSON.stringify(childWorkflowInput)}`)
} }
// Remove the workflowId from the input to avoid confusion // Remove the workflowId from the input to avoid confusion
@@ -308,10 +329,11 @@ export class WorkflowBlockHandler implements BlockHandler {
} }
return failure as Record<string, any> return failure as Record<string, any>
} }
let result = childResult
if (childResult?.output) { // childResult is an ExecutionResult with structure { success, output, metadata, logs }
result = childResult.output // We want the actual output from the execution
} const result = childResult.output || {}
return { return {
success: true, success: true,
childWorkflowName, childWorkflowName,

View File

@@ -630,17 +630,31 @@ export class Executor {
*/ */
private validateWorkflow(startBlockId?: string): void { private validateWorkflow(startBlockId?: string): void {
if (startBlockId) { if (startBlockId) {
// If starting from a specific block (webhook trigger or schedule trigger), validate that block exists
const startBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId) const startBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId)
if (!startBlock || !startBlock.enabled) { if (!startBlock || !startBlock.enabled) {
throw new Error(`Start block ${startBlockId} not found or disabled`) throw new Error(`Start block ${startBlockId} not found or disabled`)
} }
// Trigger blocks (webhook and schedule) can have incoming connections, so no need to check that return
}
const starterBlock = this.actualWorkflow.blocks.find(
(block) => block.metadata?.id === BlockType.STARTER
)
// Check for any type of trigger block (dedicated triggers or trigger-mode blocks)
const hasTriggerBlocks = this.actualWorkflow.blocks.some((block) => {
// Check if it's a dedicated trigger block (category: 'triggers')
if (block.metadata?.category === 'triggers') return true
// Check if it's a block with trigger mode enabled
if (block.config?.params?.triggerMode === true) return true
return false
})
if (hasTriggerBlocks) {
// When triggers exist (either dedicated or trigger-mode), we allow execution without a starter block
// The actual start block will be determined at runtime based on the execution context
} else { } else {
// Default validation for starter block // Legacy workflows: require a valid starter block and basic connection checks
const starterBlock = this.actualWorkflow.blocks.find(
(block) => block.metadata?.id === BlockType.STARTER
)
if (!starterBlock || !starterBlock.enabled) { if (!starterBlock || !starterBlock.enabled) {
throw new Error('Workflow must have an enabled starter block') throw new Error('Workflow must have an enabled starter block')
} }
@@ -652,22 +666,15 @@ export class Executor {
throw new Error('Starter block cannot have incoming connections') throw new Error('Starter block cannot have incoming connections')
} }
// Check if there are any trigger blocks on the canvas const outgoingFromStarter = this.actualWorkflow.connections.filter(
const hasTriggerBlocks = this.actualWorkflow.blocks.some((block) => { (conn) => conn.source === starterBlock.id
return block.metadata?.category === 'triggers' || block.config?.params?.triggerMode === true )
}) if (outgoingFromStarter.length === 0) {
throw new Error('Starter block must have at least one outgoing connection')
// Only check outgoing connections for starter blocks if there are no trigger blocks
if (!hasTriggerBlocks) {
const outgoingFromStarter = this.actualWorkflow.connections.filter(
(conn) => conn.source === starterBlock.id
)
if (outgoingFromStarter.length === 0) {
throw new Error('Starter block must have at least one outgoing connection')
}
} }
} }
// General graph validations
const blockIds = new Set(this.actualWorkflow.blocks.map((block) => block.id)) const blockIds = new Set(this.actualWorkflow.blocks.map((block) => block.id))
for (const conn of this.actualWorkflow.connections) { for (const conn of this.actualWorkflow.connections) {
if (!blockIds.has(conn.source)) { if (!blockIds.has(conn.source)) {
@@ -762,20 +769,54 @@ export class Executor {
// Determine which block to initialize as the starting point // Determine which block to initialize as the starting point
let initBlock: SerializedBlock | undefined let initBlock: SerializedBlock | undefined
if (startBlockId) { if (startBlockId) {
// Starting from a specific block (webhook trigger or schedule trigger) // Starting from a specific block (webhook trigger, schedule trigger, or new trigger blocks)
initBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId) initBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId)
} else { } else {
// Default to starter block // Default to starter block (legacy) or find any trigger block
initBlock = this.actualWorkflow.blocks.find( initBlock = this.actualWorkflow.blocks.find(
(block) => block.metadata?.id === BlockType.STARTER (block) => block.metadata?.id === BlockType.STARTER
) )
// If no starter block, look for appropriate trigger block based on context
if (!initBlock) {
if (this.isChildExecution) {
const inputTriggerBlocks = this.actualWorkflow.blocks.filter(
(block) => block.metadata?.id === 'input_trigger'
)
if (inputTriggerBlocks.length === 1) {
initBlock = inputTriggerBlocks[0]
} else if (inputTriggerBlocks.length > 1) {
throw new Error('Child workflow has multiple Input Trigger blocks. Keep only one.')
}
} else {
// Parent workflows can use any trigger block (dedicated or trigger-mode)
const triggerBlocks = this.actualWorkflow.blocks.filter(
(block) =>
block.metadata?.id === 'input_trigger' ||
block.metadata?.id === 'api_trigger' ||
block.metadata?.id === 'chat_trigger' ||
block.metadata?.category === 'triggers' ||
block.config?.params?.triggerMode === true
)
if (triggerBlocks.length > 0) {
initBlock = triggerBlocks[0]
}
}
}
} }
if (initBlock) { if (initBlock) {
// Initialize the starting block with the workflow input // Initialize the starting block with the workflow input
try { try {
// Get inputFormat from either old location (config.params) or new location (metadata.subBlocks)
const blockParams = initBlock.config.params const blockParams = initBlock.config.params
const inputFormat = blockParams?.inputFormat let inputFormat = blockParams?.inputFormat
// For new trigger blocks (api_trigger, etc), inputFormat is in metadata.subBlocks
const metadataWithSubBlocks = initBlock.metadata as any
if (!inputFormat && metadataWithSubBlocks?.subBlocks?.inputFormat?.value) {
inputFormat = metadataWithSubBlocks.subBlocks.inputFormat.value
}
// If input format is defined, structure the input according to the schema // If input format is defined, structure the input according to the schema
if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) { if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) {
@@ -841,11 +882,31 @@ export class Executor {
// Use the structured input if we processed fields, otherwise use raw input // Use the structured input if we processed fields, otherwise use raw input
const finalInput = hasProcessedFields ? structuredInput : rawInputData const finalInput = hasProcessedFields ? structuredInput : rawInputData
// Initialize the starting block with structured input (flattened) // Initialize the starting block with structured input
const blockOutput = { let blockOutput: any
input: finalInput,
conversationId: this.workflowInput?.conversationId, // Add conversationId to root // For API/Input triggers, normalize primitives and mirror objects under input
...finalInput, // Add input fields directly at top level if (
initBlock.metadata?.id === 'api_trigger' ||
initBlock.metadata?.id === 'input_trigger'
) {
const isObject =
finalInput !== null && typeof finalInput === 'object' && !Array.isArray(finalInput)
if (isObject) {
blockOutput = { ...finalInput }
// Provide a mirrored input object for universal <start.input> references
blockOutput.input = { ...finalInput }
} else {
// Primitive input: only expose under input
blockOutput = { input: finalInput }
}
} else {
// For legacy starter blocks, keep the old behavior
blockOutput = {
input: finalInput,
conversationId: this.workflowInput?.conversationId, // Add conversationId to root
...finalInput, // Add input fields directly at top level
}
} }
// Add files if present (for all trigger types) // Add files if present (for all trigger types)
@@ -863,54 +924,81 @@ export class Executor {
// This ensures files are captured in trace spans and execution logs // This ensures files are captured in trace spans and execution logs
this.createStartedBlockWithFilesLog(initBlock, blockOutput, context) this.createStartedBlockWithFilesLog(initBlock, blockOutput, context)
} else { } else {
// Handle structured input (like API calls or chat messages) // Handle triggers without inputFormat
if (this.workflowInput && typeof this.workflowInput === 'object') { let starterOutput: any
// Check if this is a chat workflow input (has both input and conversationId)
if ( // Handle different trigger types
Object.hasOwn(this.workflowInput, 'input') && if (initBlock.metadata?.id === 'chat_trigger') {
Object.hasOwn(this.workflowInput, 'conversationId') // Chat trigger: extract input, conversationId, and files
) { starterOutput = {
// Chat workflow: extract input, conversationId, and files to root level input: this.workflowInput?.input || '',
const starterOutput: any = { conversationId: this.workflowInput?.conversationId || '',
input: this.workflowInput.input, }
conversationId: this.workflowInput.conversationId,
if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) {
starterOutput.files = this.workflowInput.files
}
} else if (
initBlock.metadata?.id === 'api_trigger' ||
initBlock.metadata?.id === 'input_trigger'
) {
// API/Input trigger without inputFormat: normalize primitives and mirror objects under input
const rawCandidate =
this.workflowInput?.input !== undefined
? this.workflowInput.input
: this.workflowInput
const isObject =
rawCandidate !== null &&
typeof rawCandidate === 'object' &&
!Array.isArray(rawCandidate)
if (isObject) {
starterOutput = {
...(rawCandidate as Record<string, any>),
input: { ...(rawCandidate as Record<string, any>) },
} }
// Add files if present
if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) {
starterOutput.files = this.workflowInput.files
}
context.blockStates.set(initBlock.id, {
output: starterOutput,
executed: true,
executionTime: 0,
})
// Create a block log for the starter block if it has files
// This ensures files are captured in trace spans and execution logs
this.createStartedBlockWithFilesLog(initBlock, starterOutput, context)
} else { } else {
// API workflow: spread the raw data directly (no wrapping) starterOutput = { input: rawCandidate }
const starterOutput = { ...this.workflowInput }
context.blockStates.set(initBlock.id, {
output: starterOutput,
executed: true,
executionTime: 0,
})
} }
} else { } else {
// Fallback for primitive input values // Legacy starter block handling
const starterOutput = { if (this.workflowInput && typeof this.workflowInput === 'object') {
input: this.workflowInput, // Check if this is a chat workflow input (has both input and conversationId)
} if (
Object.hasOwn(this.workflowInput, 'input') &&
Object.hasOwn(this.workflowInput, 'conversationId')
) {
// Chat workflow: extract input, conversationId, and files to root level
starterOutput = {
input: this.workflowInput.input,
conversationId: this.workflowInput.conversationId,
}
context.blockStates.set(initBlock.id, { // Add files if present
output: starterOutput, if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) {
executed: true, starterOutput.files = this.workflowInput.files
executionTime: 0, }
}) } else {
// API workflow: spread the raw data directly (no wrapping)
starterOutput = { ...this.workflowInput }
}
} else {
// Fallback for primitive input values
starterOutput = {
input: this.workflowInput,
}
}
}
context.blockStates.set(initBlock.id, {
output: starterOutput,
executed: true,
executionTime: 0,
})
// Create a block log for the starter block if it has files
// This ensures files are captured in trace spans and execution logs
if (starterOutput.files) {
this.createStartedBlockWithFilesLog(initBlock, starterOutput, context)
} }
} }
} catch (e) { } catch (e) {

View File

@@ -1,6 +1,7 @@
import { BlockPathCalculator } from '@/lib/block-path-calculator' import { BlockPathCalculator } from '@/lib/block-path-calculator'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { VariableManager } from '@/lib/variables/variable-manager' import { VariableManager } from '@/lib/variables/variable-manager'
import { TRIGGER_REFERENCE_ALIAS_MAP } from '@/lib/workflows/triggers'
import { getBlock } from '@/blocks/index' import { getBlock } from '@/blocks/index'
import type { LoopManager } from '@/executor/loops/loops' import type { LoopManager } from '@/executor/loops/loops'
import type { ExecutionContext } from '@/executor/types' import type { ExecutionContext } from '@/executor/types'
@@ -520,15 +521,19 @@ export class InputResolver {
// System references and regular block references are both processed // System references and regular block references are both processed
// Accessibility validation happens later in validateBlockReference // Accessibility validation happens later in validateBlockReference
// Special case for "start" references // Special case for trigger block references (start, api, chat, manual)
if (blockRef.toLowerCase() === 'start') { const blockRefLower = blockRef.toLowerCase()
// Find the starter block const triggerType =
const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter') TRIGGER_REFERENCE_ALIAS_MAP[blockRefLower as keyof typeof TRIGGER_REFERENCE_ALIAS_MAP]
if (starterBlock) { if (triggerType) {
const blockState = context.blockStates.get(starterBlock.id) const triggerBlock = this.workflow.blocks.find(
(block) => block.metadata?.id === triggerType
)
if (triggerBlock) {
const blockState = context.blockStates.get(triggerBlock.id)
if (blockState) { if (blockState) {
// For starter block, start directly with the flattened output // For trigger blocks, start directly with the flattened output
// This enables direct access to <start.input> and <start.conversationId> // This enables direct access to <start.input>, <api.fieldName>, <chat.input> etc
let replacementValue: any = blockState.output let replacementValue: any = blockState.output
for (const part of pathParts) { for (const part of pathParts) {
@@ -537,7 +542,7 @@ export class InputResolver {
`[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`, `[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`,
replacementValue replacementValue
) )
throw new Error(`Invalid path "${part}" in "${path}" for starter block.`) throw new Error(`Invalid path "${part}" in "${path}" for trigger block.`)
} }
// Handle array indexing syntax like "files[0]" or "items[1]" // Handle array indexing syntax like "files[0]" or "items[1]"
@@ -550,14 +555,14 @@ export class InputResolver {
const arrayValue = replacementValue[arrayName] const arrayValue = replacementValue[arrayName]
if (!Array.isArray(arrayValue)) { if (!Array.isArray(arrayValue)) {
throw new Error( throw new Error(
`Property "${arrayName}" is not an array in path "${path}" for starter block.` `Property "${arrayName}" is not an array in path "${path}" for trigger block.`
) )
} }
// Then access the array element // Then access the array element
if (index < 0 || index >= arrayValue.length) { if (index < 0 || index >= arrayValue.length) {
throw new Error( throw new Error(
`Array index ${index} is out of bounds for "${arrayName}" (length: ${arrayValue.length}) in path "${path}" for starter block.` `Array index ${index} is out of bounds for "${arrayName}" (length: ${arrayValue.length}) in path "${path}" for trigger block.`
) )
} }
@@ -577,17 +582,22 @@ export class InputResolver {
if (replacementValue === undefined) { if (replacementValue === undefined) {
logger.warn( logger.warn(
`[resolveBlockReferences] No value found at path "${part}" in starter block.` `[resolveBlockReferences] No value found at path "${part}" in trigger block.`
) )
throw new Error(`No value found at path "${path}" in starter block.`) throw new Error(`No value found at path "${path}" in trigger block.`)
} }
} }
// Format the value based on block type and path // Format the value based on block type and path
let formattedValue: string let formattedValue: string
// Special handling for all blocks referencing starter input // Special handling for all blocks referencing trigger input
if (blockRef.toLowerCase() === 'start' && pathParts.join('.').includes('input')) { // For starter and chat triggers, check for 'input' field. For API trigger, any field access counts
const isTriggerInputRef =
(blockRefLower === 'start' && pathParts.join('.').includes('input')) ||
(blockRefLower === 'chat' && pathParts.join('.').includes('input')) ||
(blockRefLower === 'api' && pathParts.length > 0)
if (isTriggerInputRef) {
const blockType = currentBlock.metadata?.id const blockType = currentBlock.metadata?.id
// Format based on which block is consuming this value // Format based on which block is consuming this value

View File

@@ -662,7 +662,8 @@ export function useCollaborativeWorkflow() {
data?: Record<string, any>, data?: Record<string, any>,
parentId?: string, parentId?: string,
extent?: 'parent', extent?: 'parent',
autoConnectEdge?: Edge autoConnectEdge?: Edge,
triggerMode?: boolean
) => { ) => {
// Skip socket operations when in diff mode // Skip socket operations when in diff mode
if (isShowingDiff) { if (isShowingDiff) {
@@ -695,6 +696,7 @@ export function useCollaborativeWorkflow() {
horizontalHandles: true, horizontalHandles: true,
isWide: false, isWide: false,
advancedMode: false, advancedMode: false,
triggerMode: triggerMode || false,
height: 0, height: 0,
parentId, parentId,
extent, extent,
@@ -704,7 +706,7 @@ export function useCollaborativeWorkflow() {
// Skip if applying remote changes // Skip if applying remote changes
if (isApplyingRemoteChange.current) { if (isApplyingRemoteChange.current) {
workflowStore.addBlock(id, type, name, position, data, parentId, extent, { workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
triggerMode: false, triggerMode: triggerMode || false,
}) })
if (autoConnectEdge) { if (autoConnectEdge) {
workflowStore.addEdge(autoConnectEdge) workflowStore.addEdge(autoConnectEdge)
@@ -729,7 +731,7 @@ export function useCollaborativeWorkflow() {
// Apply locally first (immediate UI feedback) // Apply locally first (immediate UI feedback)
workflowStore.addBlock(id, type, name, position, data, parentId, extent, { workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
triggerMode: false, triggerMode: triggerMode || false,
}) })
if (autoConnectEdge) { if (autoConnectEdge) {
workflowStore.addEdge(autoConnectEdge) workflowStore.addEdge(autoConnectEdge)
@@ -774,7 +776,7 @@ export function useCollaborativeWorkflow() {
horizontalHandles: true, horizontalHandles: true,
isWide: false, isWide: false,
advancedMode: false, advancedMode: false,
triggerMode: false, triggerMode: triggerMode || false,
height: 0, // Default height, will be set by the UI height: 0, // Default height, will be set by the UI
parentId, parentId,
extent, extent,
@@ -801,7 +803,7 @@ export function useCollaborativeWorkflow() {
// Apply locally // Apply locally
workflowStore.addBlock(id, type, name, position, data, parentId, extent, { workflowStore.addBlock(id, type, name, position, data, parentId, extent, {
triggerMode: false, triggerMode: triggerMode || false,
}) })
if (autoConnectEdge) { if (autoConnectEdge) {
workflowStore.addEdge(autoConnectEdge) workflowStore.addEdge(autoConnectEdge)

View File

@@ -57,7 +57,16 @@ export interface SendMessageRequest {
chatId?: string chatId?: string
workflowId?: string workflowId?: string
mode?: 'ask' | 'agent' mode?: 'ask' | 'agent'
depth?: 0 | 1 | 2 | 3 model?:
| 'gpt-5-fast'
| 'gpt-5'
| 'gpt-5-medium'
| 'gpt-5-high'
| 'gpt-4o'
| 'gpt-4.1'
| 'o3'
| 'claude-4-sonnet'
| 'claude-4.1-opus'
prefetch?: boolean prefetch?: boolean
createNewChat?: boolean createNewChat?: boolean
stream?: boolean stream?: boolean

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