Skip to content

Commit e03c036

Browse files
feat(manual-trigger): add manual trigger (#1452)
* feat(manual-trigger): add manual trigger * consolidate input format extraction * exclude triggers from console logs + deployed chat error surfacing * works * centralize error messages + logging for deployed chat
1 parent 7e8ac5c commit e03c036

File tree

11 files changed

+431
-262
lines changed

11 files changed

+431
-262
lines changed

apps/sim/app/api/chat/utils.ts

Lines changed: 272 additions & 220 deletions
Large diffs are not rendered by default.

apps/sim/app/chat/[subdomain]/chat.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@ import {
1616
PasswordAuth,
1717
VoiceInterface,
1818
} from '@/app/chat/components'
19+
import { CHAT_ERROR_MESSAGES, CHAT_REQUEST_TIMEOUT_MS } from '@/app/chat/constants'
1920
import { useAudioStreaming, useChatStreaming } from '@/app/chat/hooks'
2021

2122
const logger = createLogger('ChatClient')
2223

23-
// Chat timeout configuration (5 minutes)
24-
const CHAT_REQUEST_TIMEOUT_MS = 300000
25-
2624
interface ChatConfig {
2725
id: string
2826
title: string
@@ -237,7 +235,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
237235
}
238236
} catch (error) {
239237
logger.error('Error fetching chat config:', error)
240-
setError('This chat is currently unavailable. Please try again later.')
238+
setError(CHAT_ERROR_MESSAGES.CHAT_UNAVAILABLE)
241239
}
242240
}
243241

@@ -372,7 +370,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
372370
setIsLoading(false)
373371
const errorMessage: ChatMessage = {
374372
id: crypto.randomUUID(),
375-
content: 'Sorry, there was an error processing your message. Please try again.',
373+
content: CHAT_ERROR_MESSAGES.GENERIC_ERROR,
376374
type: 'assistant',
377375
timestamp: new Date(),
378376
}

apps/sim/app/chat/constants.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const CHAT_ERROR_MESSAGES = {
2+
GENERIC_ERROR: 'Sorry, there was an error processing your message. Please try again.',
3+
NETWORK_ERROR: 'Unable to connect to the server. Please check your connection and try again.',
4+
TIMEOUT_ERROR: 'Request timed out. Please try again.',
5+
AUTH_REQUIRED_PASSWORD: 'This chat requires a password to access.',
6+
AUTH_REQUIRED_EMAIL: 'Please provide your email to access this chat.',
7+
CHAT_UNAVAILABLE: 'This chat is currently unavailable. Please try again later.',
8+
NO_CHAT_TRIGGER:
9+
'No Chat trigger configured for this workflow. Add a Chat Trigger block to enable chat execution.',
10+
USAGE_LIMIT_EXCEEDED: 'Usage limit exceeded. Please upgrade your plan to continue using chat.',
11+
} as const
12+
13+
export const CHAT_REQUEST_TIMEOUT_MS = 300000 // 5 minutes (same as in chat.tsx)
14+
15+
export type ChatErrorType = keyof typeof CHAT_ERROR_MESSAGES

apps/sim/app/chat/hooks/use-chat-streaming.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useRef, useState } from 'react'
44
import { createLogger } from '@/lib/logs/console/logger'
55
import type { ChatMessage } from '@/app/chat/components/message/message'
6+
import { CHAT_ERROR_MESSAGES } from '@/app/chat/constants'
67
// No longer need complex output extraction - backend handles this
78
import type { ExecutionResult } from '@/executor/types'
89

@@ -151,6 +152,25 @@ export function useChatStreaming() {
151152
const json = JSON.parse(line.substring(6))
152153
const { blockId, chunk: contentChunk, event: eventType } = json
153154

155+
// Handle error events from the server
156+
if (eventType === 'error' || json.event === 'error') {
157+
const errorMessage = json.error || CHAT_ERROR_MESSAGES.GENERIC_ERROR
158+
setMessages((prev) =>
159+
prev.map((msg) =>
160+
msg.id === messageId
161+
? {
162+
...msg,
163+
content: errorMessage,
164+
isStreaming: false,
165+
type: 'assistant' as const,
166+
}
167+
: msg
168+
)
169+
)
170+
setIsLoading(false)
171+
return
172+
}
173+
154174
if (eventType === 'final' && json.data) {
155175
// The backend has already processed and combined all outputs
156176
// We just need to extract the combined content and use it

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,19 @@ export function useWorkflowExecution() {
671671
selectedOutputIds = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId)
672672
}
673673

674+
// Helper to extract test values from inputFormat subblock
675+
const extractTestValuesFromInputFormat = (inputFormatValue: any): Record<string, any> => {
676+
const testInput: Record<string, any> = {}
677+
if (Array.isArray(inputFormatValue)) {
678+
inputFormatValue.forEach((field: any) => {
679+
if (field && typeof field === 'object' && field.name && field.value !== undefined) {
680+
testInput[field.name] = field.value
681+
}
682+
})
683+
}
684+
return testInput
685+
}
686+
674687
// Determine start block and workflow input based on execution type
675688
let startBlockId: string | undefined
676689
let finalWorkflowInput = workflowInput
@@ -720,19 +733,12 @@ export function useWorkflowExecution() {
720733
// Extract test values from the API trigger's inputFormat
721734
if (selectedTrigger.type === 'api_trigger' || selectedTrigger.type === 'starter') {
722735
const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value
723-
if (Array.isArray(inputFormatValue)) {
724-
const testInput: Record<string, any> = {}
725-
inputFormatValue.forEach((field: any) => {
726-
if (field && typeof field === 'object' && field.name && field.value !== undefined) {
727-
testInput[field.name] = field.value
728-
}
729-
})
736+
const testInput = extractTestValuesFromInputFormat(inputFormatValue)
730737

731-
// Use the test input as workflow input
732-
if (Object.keys(testInput).length > 0) {
733-
finalWorkflowInput = testInput
734-
logger.info('Using API trigger test values for manual run:', testInput)
735-
}
738+
// Use the test input as workflow input
739+
if (Object.keys(testInput).length > 0) {
740+
finalWorkflowInput = testInput
741+
logger.info('Using API trigger test values for manual run:', testInput)
736742
}
737743
}
738744
}
@@ -741,18 +747,29 @@ export function useWorkflowExecution() {
741747
logger.error('Multiple API triggers found')
742748
setIsExecuting(false)
743749
throw error
744-
} else if (manualTriggers.length === 1) {
745-
// No API trigger, check for manual trigger
746-
selectedTrigger = manualTriggers[0]
750+
} else if (manualTriggers.length >= 1) {
751+
// No API trigger, check for manual triggers
752+
// Prefer manual_trigger over input_trigger for simple runs
753+
const manualTrigger = manualTriggers.find((t) => t.type === 'manual_trigger')
754+
const inputTrigger = manualTriggers.find((t) => t.type === 'input_trigger')
755+
756+
selectedTrigger = manualTrigger || inputTrigger || manualTriggers[0]
747757
const blockEntry = entries.find(([, block]) => block === selectedTrigger)
748758
if (blockEntry) {
749759
selectedBlockId = blockEntry[0]
760+
761+
// Extract test values from input trigger's inputFormat if it's an input_trigger
762+
if (selectedTrigger.type === 'input_trigger') {
763+
const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value
764+
const testInput = extractTestValuesFromInputFormat(inputFormatValue)
765+
766+
// Use the test input as workflow input
767+
if (Object.keys(testInput).length > 0) {
768+
finalWorkflowInput = testInput
769+
logger.info('Using Input trigger test values for manual run:', testInput)
770+
}
771+
}
750772
}
751-
} else if (manualTriggers.length > 1) {
752-
const error = new Error('Multiple Input Trigger blocks found. Keep only one.')
753-
logger.error('Multiple input triggers found')
754-
setIsExecuting(false)
755-
throw error
756773
} else {
757774
// Fallback: Check for legacy starter block
758775
const starterBlock = Object.values(filteredStates).find((block) => block.type === 'starter')
@@ -769,8 +786,8 @@ export function useWorkflowExecution() {
769786
}
770787

771788
if (!selectedBlockId || !selectedTrigger) {
772-
const error = new Error('Manual run requires an Input Trigger or API Trigger block')
773-
logger.error('No input or API triggers found for manual run')
789+
const error = new Error('Manual run requires a Manual, Input Form, or API Trigger block')
790+
logger.error('No manual/input or API triggers found for manual run')
774791
setIsExecuting(false)
775792
throw error
776793
}

apps/sim/blocks/blocks/input_trigger.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { SVGProps } from 'react'
22
import { createElement } from 'react'
3-
import { Play } from 'lucide-react'
3+
import { FormInput } from 'lucide-react'
44
import type { BlockConfig } from '@/blocks/types'
55

6-
const InputTriggerIcon = (props: SVGProps<SVGSVGElement>) => createElement(Play, props)
6+
const InputTriggerIcon = (props: SVGProps<SVGSVGElement>) => createElement(FormInput, props)
77

88
export const InputTriggerBlock: BlockConfig = {
99
type: 'input_trigger',
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { SVGProps } from 'react'
2+
import { createElement } from 'react'
3+
import { Play } from 'lucide-react'
4+
import type { BlockConfig } from '@/blocks/types'
5+
6+
const ManualTriggerIcon = (props: SVGProps<SVGSVGElement>) => createElement(Play, props)
7+
8+
export const ManualTriggerBlock: BlockConfig = {
9+
type: 'manual_trigger',
10+
name: 'Manual',
11+
description: 'Start workflow manually from the editor',
12+
longDescription:
13+
'Trigger the workflow manually without defining an input schema. Useful for simple runs where no structured input is needed.',
14+
bestPractices: `
15+
- Use when you want a simple manual start without defining an input format.
16+
- If you need structured inputs or child workflows to map variables from, prefer the Input Form Trigger.
17+
`,
18+
category: 'triggers',
19+
bgColor: '#2563EB',
20+
icon: ManualTriggerIcon,
21+
subBlocks: [],
22+
tools: {
23+
access: [],
24+
},
25+
inputs: {},
26+
outputs: {},
27+
triggers: {
28+
enabled: true,
29+
available: ['manual'],
30+
},
31+
}

apps/sim/blocks/blocks/workflow_input.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ const getAvailableWorkflows = (): Array<{ label: string; id: string }> => {
1919
export const WorkflowInputBlock: BlockConfig = {
2020
type: 'workflow_input',
2121
name: 'Workflow',
22-
description: 'Execute another workflow and map variables to its Input Trigger schema.',
23-
longDescription: `Execute another child workflow and map variables to its Input Trigger schema. Helps with modularizing workflows.`,
22+
description: 'Execute another workflow and map variables to its Input Form Trigger schema.',
23+
longDescription: `Execute another child workflow and map variables to its Input Form Trigger schema. Helps with modularizing workflows.`,
2424
bestPractices: `
2525
- Usually clarify/check if the user has tagged a workflow to use as the child workflow. Understand the child workflow to determine the logical position of this block in the workflow.
2626
- Remember, that the start point of the child workflow is the Input Form Trigger block.

apps/sim/blocks/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { JiraBlock } from '@/blocks/blocks/jira'
3838
import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
3939
import { LinearBlock } from '@/blocks/blocks/linear'
4040
import { LinkupBlock } from '@/blocks/blocks/linkup'
41+
import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger'
4142
import { McpBlock } from '@/blocks/blocks/mcp'
4243
import { Mem0Block } from '@/blocks/blocks/mem0'
4344
import { MemoryBlock } from '@/blocks/blocks/memory'
@@ -153,6 +154,7 @@ export const registry: Record<string, BlockConfig> = {
153154
starter: StarterBlock,
154155
input_trigger: InputTriggerBlock,
155156
chat_trigger: ChatTriggerBlock,
157+
manual_trigger: ManualTriggerBlock,
156158
api_trigger: ApiTriggerBlock,
157159
supabase: SupabaseBlock,
158160
tavily: TavilyBlock,

apps/sim/executor/index.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BlockPathCalculator } from '@/lib/block-path-calculator'
22
import { createLogger } from '@/lib/logs/console/logger'
33
import type { TraceSpan } from '@/lib/logs/types'
4+
import { getBlock } from '@/blocks'
45
import type { BlockOutput } from '@/blocks/types'
56
import { BlockType } from '@/executor/consts'
67
import {
@@ -1779,9 +1780,16 @@ export class Executor {
17791780

17801781
context.blockLogs.push(blockLog)
17811782

1782-
// Skip console logging for infrastructure blocks like loops and parallels
1783+
// Skip console logging for infrastructure blocks and trigger blocks
17831784
// For streaming blocks, we'll add the console entry after stream processing
1784-
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
1785+
const blockConfig = getBlock(block.metadata?.id || '')
1786+
const isTriggerBlock =
1787+
blockConfig?.category === 'triggers' || block.metadata?.id === BlockType.STARTER
1788+
if (
1789+
block.metadata?.id !== BlockType.LOOP &&
1790+
block.metadata?.id !== BlockType.PARALLEL &&
1791+
!isTriggerBlock
1792+
) {
17851793
// Determine iteration context for this block
17861794
let iterationCurrent: number | undefined
17871795
let iterationTotal: number | undefined
@@ -1889,8 +1897,15 @@ export class Executor {
18891897

18901898
context.blockLogs.push(blockLog)
18911899

1892-
// Skip console logging for infrastructure blocks like loops and parallels
1893-
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
1900+
// Skip console logging for infrastructure blocks and trigger blocks
1901+
const nonStreamBlockConfig = getBlock(block.metadata?.id || '')
1902+
const isNonStreamTriggerBlock =
1903+
nonStreamBlockConfig?.category === 'triggers' || block.metadata?.id === BlockType.STARTER
1904+
if (
1905+
block.metadata?.id !== BlockType.LOOP &&
1906+
block.metadata?.id !== BlockType.PARALLEL &&
1907+
!isNonStreamTriggerBlock
1908+
) {
18941909
// Determine iteration context for this block
18951910
let iterationCurrent: number | undefined
18961911
let iterationTotal: number | undefined
@@ -2001,8 +2016,15 @@ export class Executor {
20012016
// Log the error even if we'll continue execution through error path
20022017
context.blockLogs.push(blockLog)
20032018

2004-
// Skip console logging for infrastructure blocks like loops and parallels
2005-
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
2019+
// Skip console logging for infrastructure blocks and trigger blocks
2020+
const errorBlockConfig = getBlock(block.metadata?.id || '')
2021+
const isErrorTriggerBlock =
2022+
errorBlockConfig?.category === 'triggers' || block.metadata?.id === BlockType.STARTER
2023+
if (
2024+
block.metadata?.id !== BlockType.LOOP &&
2025+
block.metadata?.id !== BlockType.PARALLEL &&
2026+
!isErrorTriggerBlock
2027+
) {
20062028
// Determine iteration context for this block
20072029
let iterationCurrent: number | undefined
20082030
let iterationTotal: number | undefined

0 commit comments

Comments
 (0)