From 7f1ff7fd86cf37b693fe734cbff305c06b4f62e0 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 25 Oct 2025 09:59:57 -1000 Subject: [PATCH 1/8] fix(billing): should allow restoring subscription (#1728) * fix(already-cancelled-sub): UI should allow restoring subscription * restore functionality fixed * fix --- apps/sim/app/api/billing/portal/route.ts | 7 +- .../cancel-subscription.tsx | 106 +++++++++--------- .../components/subscription/subscription.tsx | 1 + apps/sim/lib/billing/core/billing.ts | 12 ++ apps/sim/stores/subscription/types.ts | 1 + 5 files changed, 70 insertions(+), 57 deletions(-) diff --git a/apps/sim/app/api/billing/portal/route.ts b/apps/sim/app/api/billing/portal/route.ts index 017fbb8bd7..959a83cd7f 100644 --- a/apps/sim/app/api/billing/portal/route.ts +++ b/apps/sim/app/api/billing/portal/route.ts @@ -1,6 +1,6 @@ import { db } from '@sim/db' import { subscription as subscriptionTable, user } from '@sim/db/schema' -import { and, eq } from 'drizzle-orm' +import { and, eq, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { requireStripeClient } from '@/lib/billing/stripe-client' @@ -38,7 +38,10 @@ export async function POST(request: NextRequest) { .where( and( eq(subscriptionTable.referenceId, organizationId), - eq(subscriptionTable.status, 'active') + or( + eq(subscriptionTable.status, 'active'), + eq(subscriptionTable.cancelAtPeriodEnd, true) + ) ) ) .limit(1) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx index 1f5ea569aa..fd81cec55e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/components/cancel-subscription/cancel-subscription.tsx @@ -12,7 +12,6 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { useSession, useSubscription } from '@/lib/auth-client' import { createLogger } from '@/lib/logs/console/logger' import { getBaseUrl } from '@/lib/urls/utils' @@ -30,6 +29,7 @@ interface CancelSubscriptionProps { } subscriptionData?: { periodEnd?: Date | null + cancelAtPeriodEnd?: boolean } } @@ -127,35 +127,48 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub const subscriptionStatus = getSubscriptionStatus() const activeOrgId = activeOrganization?.id - // For team/enterprise plans, get the subscription ID from organization store - if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) { - const orgSubscription = useOrganizationStore.getState().subscriptionData + if (isCancelAtPeriodEnd) { + if (!betterAuthSubscription.restore) { + throw new Error('Subscription restore not available') + } + + let referenceId: string + let subscriptionId: string | undefined + + if ((subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) && activeOrgId) { + const orgSubscription = useOrganizationStore.getState().subscriptionData + referenceId = activeOrgId + subscriptionId = orgSubscription?.id + } else { + // For personal subscriptions, use user ID and let better-auth find the subscription + referenceId = session.user.id + subscriptionId = undefined + } + + logger.info('Restoring subscription', { referenceId, subscriptionId }) - if (orgSubscription?.id && orgSubscription?.cancelAtPeriodEnd) { - // Restore the organization subscription - if (!betterAuthSubscription.restore) { - throw new Error('Subscription restore not available') - } - - const result = await betterAuthSubscription.restore({ - referenceId: activeOrgId, - subscriptionId: orgSubscription.id, - }) - logger.info('Organization subscription restored successfully', result) + // Build restore params - only include subscriptionId if we have one (team/enterprise) + const restoreParams: any = { referenceId } + if (subscriptionId) { + restoreParams.subscriptionId = subscriptionId } + + const result = await betterAuthSubscription.restore(restoreParams) + + logger.info('Subscription restored successfully', result) } - // Refresh state and close await refresh() if (activeOrgId) { await loadOrganizationSubscription(activeOrgId) await refreshOrganization().catch(() => {}) } + setIsDialogOpen(false) } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to keep subscription' + const errorMessage = error instanceof Error ? error.message : 'Failed to restore subscription' setError(errorMessage) - logger.error('Failed to keep subscription', { error }) + logger.error('Failed to restore subscription', { error }) } finally { setIsLoading(false) } @@ -190,19 +203,15 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub const periodEndDate = getPeriodEndDate() // Check if subscription is set to cancel at period end - const isCancelAtPeriodEnd = (() => { - const subscriptionStatus = getSubscriptionStatus() - if (subscriptionStatus.isTeam || subscriptionStatus.isEnterprise) { - return useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd === true - } - return false - })() + const isCancelAtPeriodEnd = subscriptionData?.cancelAtPeriodEnd === true return ( <>
- Manage Subscription + + {isCancelAtPeriodEnd ? 'Restore Subscription' : 'Manage Subscription'} + {isCancelAtPeriodEnd && (

You'll keep access until {formatDate(periodEndDate)} @@ -217,10 +226,12 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub 'h-8 rounded-[8px] font-medium text-xs transition-all duration-200', error ? 'border-red-500 text-red-500 dark:border-red-500 dark:text-red-500' - : 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500' + : isCancelAtPeriodEnd + ? 'text-muted-foreground hover:border-green-500 hover:bg-green-500 hover:text-white dark:hover:border-green-500 dark:hover:bg-green-500' + : 'text-muted-foreground hover:border-red-500 hover:bg-red-500 hover:text-white dark:hover:border-red-500 dark:hover:bg-red-500' )} > - {error ? 'Error' : 'Manage'} + {error ? 'Error' : isCancelAtPeriodEnd ? 'Restore' : 'Manage'}

@@ -228,11 +239,11 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub - {isCancelAtPeriodEnd ? 'Manage' : 'Cancel'} {subscription.plan} subscription? + {isCancelAtPeriodEnd ? 'Restore' : 'Cancel'} {subscription.plan} subscription? {isCancelAtPeriodEnd - ? 'Your subscription is set to cancel at the end of the billing period. You can reactivate it or manage other settings.' + ? 'Your subscription is set to cancel at the end of the billing period. Would you like to keep your subscription active?' : `You'll be redirected to Stripe to manage your subscription. You'll keep access until ${formatDate( periodEndDate )}, then downgrade to free plan.`}{' '} @@ -260,38 +271,23 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub setIsDialogOpen(false) : handleKeep} disabled={isLoading} > - Keep Subscription + {isCancelAtPeriodEnd ? 'Cancel' : 'Keep Subscription'} {(() => { const subscriptionStatus = getSubscriptionStatus() - if ( - subscriptionStatus.isPaid && - (activeOrganization?.id - ? useOrganizationStore.getState().subscriptionData?.cancelAtPeriodEnd - : false) - ) { + if (subscriptionStatus.isPaid && isCancelAtPeriodEnd) { return ( - - - -
- - Continue - -
-
- -

Subscription will be cancelled at end of billing period

-
-
-
+ + {isLoading ? 'Restoring...' : 'Restore Subscription'} + ) } return ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx index 9ac78581ef..b69b499aff 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx @@ -523,6 +523,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) { }} subscriptionData={{ periodEnd: subscriptionData?.periodEnd || null, + cancelAtPeriodEnd: subscriptionData?.cancelAtPeriodEnd, }} />
diff --git a/apps/sim/lib/billing/core/billing.ts b/apps/sim/lib/billing/core/billing.ts index 48085eb821..55b6a207f7 100644 --- a/apps/sim/lib/billing/core/billing.ts +++ b/apps/sim/lib/billing/core/billing.ts @@ -220,6 +220,7 @@ export async function getSimplifiedBillingSummary( metadata: any stripeSubscriptionId: string | null periodEnd: Date | string | null + cancelAtPeriodEnd?: boolean // Usage details usage: { current: number @@ -318,6 +319,7 @@ export async function getSimplifiedBillingSummary( metadata: subscription.metadata || null, stripeSubscriptionId: subscription.stripeSubscriptionId || null, periodEnd: subscription.periodEnd || null, + cancelAtPeriodEnd: subscription.cancelAtPeriodEnd || undefined, // Usage details usage: { current: usageData.currentUsage, @@ -393,6 +395,7 @@ export async function getSimplifiedBillingSummary( metadata: subscription?.metadata || null, stripeSubscriptionId: subscription?.stripeSubscriptionId || null, periodEnd: subscription?.periodEnd || null, + cancelAtPeriodEnd: subscription?.cancelAtPeriodEnd || undefined, // Usage details usage: { current: currentUsage, @@ -450,5 +453,14 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') { lastPeriodCost: 0, daysRemaining: 0, }, + ...(type === 'organization' && { + organizationData: { + seatCount: 0, + memberCount: 0, + totalBasePrice: 0, + totalCurrentUsage: 0, + totalOverage: 0, + }, + }), } } diff --git a/apps/sim/stores/subscription/types.ts b/apps/sim/stores/subscription/types.ts index c0de147d45..643694b795 100644 --- a/apps/sim/stores/subscription/types.ts +++ b/apps/sim/stores/subscription/types.ts @@ -29,6 +29,7 @@ export interface SubscriptionData { metadata: any | null stripeSubscriptionId: string | null periodEnd: Date | null + cancelAtPeriodEnd?: boolean usage: UsageData billingBlocked?: boolean } From 6d1b93e80a3042838baf5e128ea9fdee8a62fa78 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 27 Oct 2025 21:13:48 -0700 Subject: [PATCH 2/8] improvement(start): revert to start block --- apps/docs/content/docs/en/triggers/index.mdx | 29 ++-- apps/docs/content/docs/en/triggers/meta.json | 2 +- .../app/api/workflows/[id]/execute/route.ts | 12 +- .../app/api/workflows/[id]/status/route.ts | 9 +- .../components/deploy-modal/deploy-modal.tsx | 6 +- .../components/control-bar/control-bar.tsx | 25 ++-- .../hooks/use-workflow-execution.ts | 17 ++- apps/sim/blocks/blocks/api_trigger.ts | 5 +- apps/sim/blocks/blocks/chat_trigger.ts | 3 +- apps/sim/blocks/blocks/input_trigger.ts | 5 +- apps/sim/blocks/blocks/manual_trigger.ts | 5 +- apps/sim/blocks/blocks/start_trigger.ts | 38 +++++ apps/sim/blocks/registry.ts | 2 + apps/sim/executor/index.ts | 66 +++++++-- apps/sim/lib/workflows/block-outputs.ts | 134 +++++++++++------- apps/sim/lib/workflows/triggers.ts | 45 ++++-- 16 files changed, 280 insertions(+), 123 deletions(-) create mode 100644 apps/sim/blocks/blocks/start_trigger.ts diff --git a/apps/docs/content/docs/en/triggers/index.mdx b/apps/docs/content/docs/en/triggers/index.mdx index 4976f27925..6bedd5918a 100644 --- a/apps/docs/content/docs/en/triggers/index.mdx +++ b/apps/docs/content/docs/en/triggers/index.mdx @@ -7,46 +7,35 @@ import { Card, Cards } from 'fumadocs-ui/components/card' ## Core Triggers -Pick one trigger per workflow to define how it starts: +Use the Start block for everything originating from the editor, deploy-to-API, or deploy-to-chat experiences. Other triggers remain available for event-driven workflows: - - HTTP endpoint that maps JSON bodies into workflow inputs + + Unified entry point that supports editor runs, API deployments and chat deployments - - Deployed chat interface with streaming responses - - - Typed manual input used in editor runs and child workflows - - - On-demand runs with no additional data + + Receive external webhook payloads Cron or interval based execution - - Receive external webhook payloads - ## Quick Comparison | Trigger | Start condition | |---------|-----------------| -| **API** | Authenticated HTTP POST | -| **Chat** | Chat deployment message | -| **Input Form** | On manual submit in editor or parent workflow | -| **Manual** | Run button in editor | +| **Start** | Editor runs, deploy-to-API requests, or chat messages | | **Schedule** | Timer managed in schedule modal | | **Webhook** | On inbound HTTP request | +> The Start block always exposes `input`, `conversationId`, and `files` fields. Add custom fields to the input format for additional structured data. + ## Using Triggers -1. Drop the trigger block in the start slot. +1. Drop the Start block in the start slot (or an alternate trigger like Webhook/Schedule). 2. Configure any required schema or auth. 3. Connect the block to the rest of the workflow. > Deployments power every trigger. Update the workflow, redeploy, and all trigger entry points pick up the new snapshot. Learn more in [Execution → Deployment Snapshots](/execution). -Legacy Starter blocks remain for existing flows but no longer appear in new builds. diff --git a/apps/docs/content/docs/en/triggers/meta.json b/apps/docs/content/docs/en/triggers/meta.json index a63ecb69d9..008a6ab1ee 100644 --- a/apps/docs/content/docs/en/triggers/meta.json +++ b/apps/docs/content/docs/en/triggers/meta.json @@ -1,3 +1,3 @@ { - "pages": ["index", "api", "chat", "input-form", "manual", "schedule", "starter", "webhook"] + "pages": ["index", "start", "schedule", "webhook", "starter"] } diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 5723028a26..0f67436d25 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -553,7 +553,7 @@ export async function POST( : undefined), workflowTriggerType: body.workflowTriggerType || (isInternalCall && body.stream ? 'chat' : 'api'), - input: body.input !== undefined ? body.input : body, + input: body, } } @@ -576,13 +576,19 @@ export async function POST( const blocks = deployedData.blocks || {} logger.info(`[${requestId}] Loaded ${Object.keys(blocks).length} blocks from workflow`) + const startTriggerBlock = Object.values(blocks).find( + (block: any) => block.type === 'start_trigger' + ) as any const apiTriggerBlock = Object.values(blocks).find( (block: any) => block.type === 'api_trigger' ) as any + logger.info(`[${requestId}] Start trigger block found:`, !!startTriggerBlock) logger.info(`[${requestId}] API trigger block found:`, !!apiTriggerBlock) - if (apiTriggerBlock?.subBlocks?.inputFormat?.value) { - const inputFormat = apiTriggerBlock.subBlocks.inputFormat.value as Array<{ + const triggerBlock = startTriggerBlock || apiTriggerBlock + + if (triggerBlock?.subBlocks?.inputFormat?.value) { + const inputFormat = triggerBlock.subBlocks.inputFormat.value as Array<{ name: string type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files' }> diff --git a/apps/sim/app/api/workflows/[id]/status/route.ts b/apps/sim/app/api/workflows/[id]/status/route.ts index c89af4ed6f..36161f5ad4 100644 --- a/apps/sim/app/api/workflows/[id]/status/route.ts +++ b/apps/sim/app/api/workflows/[id]/status/route.ts @@ -31,7 +31,14 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const normalizedData = await loadWorkflowFromNormalizedTables(id) if (!normalizedData) { - return createErrorResponse('Failed to load workflow state', 500) + // Workflow exists but has no blocks in normalized tables (empty workflow or not migrated) + // This is valid state - return success with no redeployment needed + return createSuccessResponse({ + isDeployed: validation.workflow.isDeployed, + deployedAt: validation.workflow.deployedAt, + isPublished: validation.workflow.isPublished, + needsRedeployment: false, + }) } const currentState = { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx index 601a6f5f16..00eb9d679a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx @@ -121,12 +121,12 @@ export function DeployModal({ try { const blocks = Object.values(useWorkflowStore.getState().blocks) - // Check for API trigger block first (takes precedence) + // Check for start_trigger first, then API trigger, then legacy starter + const startTriggerBlock = blocks.find((block) => block.type === 'start_trigger') const apiTriggerBlock = blocks.find((block) => block.type === 'api_trigger') - // Fall back to legacy starter block const starterBlock = blocks.find((block) => block.type === 'starter') - const targetBlock = apiTriggerBlock || starterBlock + const targetBlock = startTriggerBlock || apiTriggerBlock || starterBlock if (targetBlock) { const inputFormat = useSubBlockStore.getState().getValue(targetBlock.id, 'inputFormat') diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx index 975d27ba4e..33cd1c003e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { Bug, ChevronLeft, @@ -47,6 +47,7 @@ import { getKeyboardShortcutText, useKeyboardShortcuts, } from '@/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts' +import { useDebounce } from '@/hooks/use-debounce' import { useFolderStore } from '@/stores/folders/store' import { useOperationQueueStore } from '@/stores/operation-queue/store' import { usePanelStore } from '@/stores/panel/store' @@ -265,6 +266,17 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { activeWorkflowId ? state.workflowValues[activeWorkflowId] : null ) + const statusCheckTrigger = useMemo(() => { + return JSON.stringify({ + blocks: Object.keys(currentBlocks || {}).length, + edges: currentEdges?.length || 0, + subBlocks: Object.keys(subBlockValues || {}).length, + timestamp: Date.now(), + }) + }, [currentBlocks, currentEdges, subBlockValues]) + + const debouncedStatusCheckTrigger = useDebounce(statusCheckTrigger, 500) + useEffect(() => { // Avoid off-by-one false positives: wait until operation queue is idle const { operations, isProcessing } = useOperationQueueStore.getState() @@ -299,16 +311,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { } checkForChanges() - }, [ - activeWorkflowId, - deployedState, - currentBlocks, - currentEdges, - subBlockValues, - isLoadingDeployedState, - useOperationQueueStore.getState().isProcessing, - useOperationQueueStore.getState().operations.length, - ]) + }, [activeWorkflowId, deployedState, debouncedStatusCheckTrigger, isLoadingDeployedState]) useEffect(() => { if (session?.user?.id && !isRegistryLoading) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 5e2d019321..4f77624b93 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -857,14 +857,18 @@ export function useWorkflowExecution() { selectedBlockId = blockEntry[0] // Extract test values from the API trigger's inputFormat - if (selectedTrigger.type === 'api_trigger' || selectedTrigger.type === 'starter') { + if ( + selectedTrigger.type === 'api_trigger' || + selectedTrigger.type === 'start_trigger' || + selectedTrigger.type === 'starter' + ) { const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value const testInput = extractTestValuesFromInputFormat(inputFormatValue) // 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) + logger.info('Using trigger test values for manual run:', testInput) } } } @@ -884,15 +888,18 @@ export function useWorkflowExecution() { if (blockEntry) { selectedBlockId = blockEntry[0] - // Extract test values from input trigger's inputFormat if it's an input_trigger - if (selectedTrigger.type === 'input_trigger') { + // Extract test values from input trigger's inputFormat if it's an input_trigger or start_trigger + if ( + selectedTrigger.type === 'input_trigger' || + selectedTrigger.type === 'start_trigger' + ) { const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value const testInput = extractTestValuesFromInputFormat(inputFormatValue) // Use the test input as workflow input if (Object.keys(testInput).length > 0) { finalWorkflowInput = testInput - logger.info('Using Input trigger test values for manual run:', testInput) + logger.info('Using trigger test values for manual run:', testInput) } } } diff --git a/apps/sim/blocks/blocks/api_trigger.ts b/apps/sim/blocks/blocks/api_trigger.ts index 5a830f9b6c..c53544303b 100644 --- a/apps/sim/blocks/blocks/api_trigger.ts +++ b/apps/sim/blocks/blocks/api_trigger.ts @@ -4,8 +4,8 @@ import type { BlockConfig } from '@/blocks/types' export const ApiTriggerBlock: BlockConfig = { type: 'api_trigger', triggerAllowed: true, - name: 'API', - description: 'Expose as HTTP API endpoint', + name: 'API (Legacy)', + description: 'Legacy block for exposing HTTP API endpoint. Prefer Start block.', longDescription: 'API trigger to start the workflow via authenticated HTTP calls with structured input.', bestPractices: ` @@ -14,6 +14,7 @@ export const ApiTriggerBlock: BlockConfig = { - In production, the curl would come in as e.g. curl -X POST -H "X-API-Key: $SIM_API_KEY" -H "Content-Type: application/json" -d '{"paramName":"example"}' https://www.staging.sim.ai/api/workflows/9e7e4f26-fc5e-4659-b270-7ea474b14f4a/execute -- If user asks to test via API, you might need to clarify the API key. `, category: 'triggers', + hideFromToolbar: true, bgColor: '#2F55FF', icon: ApiIcon, subBlocks: [ diff --git a/apps/sim/blocks/blocks/chat_trigger.ts b/apps/sim/blocks/blocks/chat_trigger.ts index 145cdc89b5..2efb6612fc 100644 --- a/apps/sim/blocks/blocks/chat_trigger.ts +++ b/apps/sim/blocks/blocks/chat_trigger.ts @@ -9,12 +9,13 @@ export const ChatTriggerBlock: BlockConfig = { type: 'chat_trigger', triggerAllowed: true, name: 'Chat', - description: 'Start workflow from a chat deployment', + description: 'Legacy chat start block. Prefer the unified Start block.', longDescription: 'Chat trigger to run the workflow via deployed chat interfaces.', bestPractices: ` - Can run the workflow manually to test implementation when this is the trigger point by passing in a message. `, category: 'triggers', + hideFromToolbar: true, bgColor: '#6F3DFA', icon: ChatTriggerIcon, subBlocks: [], diff --git a/apps/sim/blocks/blocks/input_trigger.ts b/apps/sim/blocks/blocks/input_trigger.ts index 073c38ad7e..c0f9eb0205 100644 --- a/apps/sim/blocks/blocks/input_trigger.ts +++ b/apps/sim/blocks/blocks/input_trigger.ts @@ -8,8 +8,8 @@ const InputTriggerIcon = (props: SVGProps) => createElement(FormI export const InputTriggerBlock: BlockConfig = { type: 'input_trigger', triggerAllowed: true, - name: 'Input Form', - description: 'Start workflow manually with a defined input schema', + name: 'Input Form (Legacy)', + description: 'Legacy manual start block with structured input. Prefer Start block.', longDescription: 'Manually trigger the workflow from the editor with a structured input schema. This enables typed inputs for parent workflows to map into.', bestPractices: ` @@ -18,6 +18,7 @@ export const InputTriggerBlock: BlockConfig = { - Also used in child workflows to map variables from the parent workflow. `, category: 'triggers', + hideFromToolbar: true, bgColor: '#3B82F6', icon: InputTriggerIcon, subBlocks: [ diff --git a/apps/sim/blocks/blocks/manual_trigger.ts b/apps/sim/blocks/blocks/manual_trigger.ts index 4d8a433d6d..ded5616efb 100644 --- a/apps/sim/blocks/blocks/manual_trigger.ts +++ b/apps/sim/blocks/blocks/manual_trigger.ts @@ -8,8 +8,8 @@ const ManualTriggerIcon = (props: SVGProps) => createElement(Play export const ManualTriggerBlock: BlockConfig = { type: 'manual_trigger', triggerAllowed: true, - name: 'Manual', - description: 'Start workflow manually from the editor', + name: 'Manual (Legacy)', + description: 'Legacy manual start block. Prefer the Start block.', longDescription: 'Trigger the workflow manually without defining an input schema. Useful for simple runs where no structured input is needed.', bestPractices: ` @@ -17,6 +17,7 @@ export const ManualTriggerBlock: BlockConfig = { - If you need structured inputs or child workflows to map variables from, prefer the Input Form Trigger. `, category: 'triggers', + hideFromToolbar: true, bgColor: '#2563EB', icon: ManualTriggerIcon, subBlocks: [], diff --git a/apps/sim/blocks/blocks/start_trigger.ts b/apps/sim/blocks/blocks/start_trigger.ts new file mode 100644 index 0000000000..411d531827 --- /dev/null +++ b/apps/sim/blocks/blocks/start_trigger.ts @@ -0,0 +1,38 @@ +import { StartIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' + +export const StartTriggerBlock: BlockConfig = { + type: 'start_trigger', + triggerAllowed: true, + name: 'Start', + description: 'Unified workflow entry point for chat, manual and API runs', + longDescription: + 'Collect structured inputs and power manual runs, API executions, and deployed chat experiences from a single start block.', + bestPractices: ` + - The Start block always exposes "input", "conversationId", and "files" fields for chat compatibility. + - Add custom input format fields to collect additional structured data. + - Test manual runs by pre-filling default values inside the input format fields. + `, + category: 'triggers', + bgColor: '#2563EB', + icon: StartIcon, + hideFromToolbar: false, + subBlocks: [ + { + id: 'inputFormat', + title: 'Input Format', + type: 'input-format', + layout: 'full', + description: 'Add custom fields beyond the built-in input, conversationId, and files fields.', + }, + ], + tools: { + access: [], + }, + inputs: {}, + outputs: {}, + triggers: { + enabled: true, + available: ['chat', 'manual', 'api'], + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index f0d4465cd6..2a16a9509f 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -65,6 +65,7 @@ import { SharepointBlock } from '@/blocks/blocks/sharepoint' import { SlackBlock } from '@/blocks/blocks/slack' import { StagehandBlock } from '@/blocks/blocks/stagehand' import { StagehandAgentBlock } from '@/blocks/blocks/stagehand_agent' +import { StartTriggerBlock } from '@/blocks/blocks/start_trigger' import { StarterBlock } from '@/blocks/blocks/starter' import { SupabaseBlock } from '@/blocks/blocks/supabase' import { TavilyBlock } from '@/blocks/blocks/tavily' @@ -154,6 +155,7 @@ export const registry: Record = { stagehand_agent: StagehandAgentBlock, slack: SlackBlock, starter: StarterBlock, + start_trigger: StartTriggerBlock, input_trigger: InputTriggerBlock, chat_trigger: ChatTriggerBlock, manual_trigger: ManualTriggerBlock, diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index e5d6230cdc..1af8af8238 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -836,8 +836,10 @@ export class Executor { ? this.workflowInput.input[field.name] // Try to get from input.field : this.workflowInput?.[field.name] // Fallback to direct field access + // Only use test values for manual editor runs, not for deployed executions (API/webhook/schedule/chat) + const isDeployedExecution = this.contextExtensions?.isDeployedContext === true if (inputValue === undefined || inputValue === null) { - if (Object.hasOwn(field, 'value')) { + if (!isDeployedExecution && Object.hasOwn(field, 'value')) { inputValue = (field as any).value } } @@ -887,8 +889,29 @@ export class Executor { // Initialize the starting block with structured input let blockOutput: any - // For API/Input triggers, normalize primitives and mirror objects under input - if ( + // For Start/API/Input triggers, normalize primitives and mirror objects under input + const isStartTrigger = initBlock.metadata?.id === 'start_trigger' + if (isStartTrigger) { + // Start trigger: pass through entire workflowInput payload + set reserved fields + blockOutput = {} + + // First, spread the entire workflowInput (all fields from API payload) + if (this.workflowInput && typeof this.workflowInput === 'object') { + for (const [key, value] of Object.entries(this.workflowInput)) { + if (key !== 'onUploadError') { + blockOutput[key] = value + } + } + } + + // Then ensure reserved fields are always set correctly for chat compatibility + if (!blockOutput.input) { + blockOutput.input = '' + } + if (!blockOutput.conversationId) { + blockOutput.conversationId = '' + } + } else if ( initBlock.metadata?.id === 'api_trigger' || initBlock.metadata?.id === 'input_trigger' ) { @@ -896,12 +919,17 @@ export class Executor { finalInput !== null && typeof finalInput === 'object' && !Array.isArray(finalInput) if (isObject) { blockOutput = { ...finalInput } - // Provide a mirrored input object for universal references + // Provide a mirrored input object for / legacy references blockOutput.input = { ...finalInput } } else { // Primitive input: only expose under input blockOutput = { input: finalInput } } + + // Add files if present (for all trigger types) + if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) { + blockOutput.files = this.workflowInput.files + } } else { // For legacy starter blocks, keep the old behavior blockOutput = { @@ -909,11 +937,11 @@ export class Executor { conversationId: this.workflowInput?.conversationId, // Add conversationId to root ...finalInput, // Add input fields directly at top level } - } - // Add files if present (for all trigger types) - if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) { - blockOutput.files = this.workflowInput.files + // Add files if present (for all trigger types) + if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) { + blockOutput.files = this.workflowInput.files + } } context.blockStates.set(initBlock.id, { @@ -930,7 +958,27 @@ export class Executor { let starterOutput: any // Handle different trigger types - if (initBlock.metadata?.id === 'chat_trigger') { + if (initBlock.metadata?.id === 'start_trigger') { + // Start trigger without inputFormat: pass through entire payload + ensure reserved fields + starterOutput = {} + + // Pass through entire workflowInput + if (this.workflowInput && typeof this.workflowInput === 'object') { + for (const [key, value] of Object.entries(this.workflowInput)) { + if (key !== 'onUploadError') { + starterOutput[key] = value + } + } + } + + // Ensure reserved fields are always set + if (!starterOutput.input) { + starterOutput.input = '' + } + if (!starterOutput.conversationId) { + starterOutput.conversationId = '' + } + } else if (initBlock.metadata?.id === 'chat_trigger') { // Chat trigger: extract input, conversationId, and files starterOutput = { input: this.workflowInput?.input || '', diff --git a/apps/sim/lib/workflows/block-outputs.ts b/apps/sim/lib/workflows/block-outputs.ts index ada820a686..e53c3e85f7 100644 --- a/apps/sim/lib/workflows/block-outputs.ts +++ b/apps/sim/lib/workflows/block-outputs.ts @@ -2,6 +2,41 @@ import { getBlock } from '@/blocks' import type { BlockConfig } from '@/blocks/types' import { getTrigger } from '@/triggers' +type InputFormatField = { name?: string; type?: string | undefined | null } + +function normalizeInputFormatValue(inputFormatValue: any): InputFormatField[] { + if ( + inputFormatValue === null || + inputFormatValue === undefined || + (Array.isArray(inputFormatValue) && inputFormatValue.length === 0) + ) { + return [] + } + + if (!Array.isArray(inputFormatValue)) { + return [] + } + + return inputFormatValue.filter((field) => field && typeof field === 'object') +} + +function applyInputFormatFields( + inputFormat: InputFormatField[], + outputs: Record +): Record { + for (const field of inputFormat) { + const fieldName = field?.name?.trim() + if (!fieldName) continue + + outputs[fieldName] = { + type: (field?.type || 'any') as any, + description: `Field from input format`, + } + } + + return outputs +} + /** * Get the effective outputs for a block, including dynamic outputs from inputFormat * and trigger outputs for blocks in trigger mode @@ -14,7 +49,6 @@ export function getBlockOutputs( const blockConfig = getBlock(blockType) if (!blockConfig) return {} - // If block is in trigger mode, use trigger outputs instead of block outputs if (triggerMode && blockConfig.triggers?.enabled) { const triggerId = subBlocks?.triggerId?.value || blockConfig.triggers?.available?.[0] if (triggerId) { @@ -25,9 +59,29 @@ export function getBlockOutputs( } } - // Start with the static outputs defined in the config let outputs = { ...(blockConfig.outputs || {}) } + if (blockType === 'start_trigger') { + outputs = { + input: { type: 'string', description: 'Primary user input or message' }, + conversationId: { type: 'string', description: 'Conversation thread identifier' }, + files: { type: 'files', description: 'User uploaded files' }, + } + + const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value) + for (const field of normalizedInputFormat) { + const fieldName = field?.name?.trim() + if (!fieldName) continue + + outputs[fieldName] = { + type: (field?.type || 'any') as any, + description: `Field from input format`, + } + } + + return outputs + } + // Special handling for starter block (legacy) if (blockType === 'starter') { const startWorkflowValue = subBlocks?.startWorkflow?.value @@ -46,70 +100,37 @@ export function getBlockOutputs( startWorkflowValue === 'manual' ) { // API/manual mode - use inputFormat fields only - let inputFormatValue = subBlocks?.inputFormat?.value + const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value) outputs = {} - - if ( - inputFormatValue !== null && - inputFormatValue !== undefined && - !Array.isArray(inputFormatValue) - ) { - inputFormatValue = [] - } - - if (Array.isArray(inputFormatValue)) { - inputFormatValue.forEach((field: { name?: string; type?: string }) => { - if (field?.name && field.name.trim() !== '') { - outputs[field.name] = { - type: (field.type || 'any') as any, - description: `Field from input format`, - } - } - }) - } - - return outputs + return applyInputFormatFields(normalizedInputFormat, outputs) } } // For blocks with inputFormat, add dynamic outputs if (hasInputFormat(blockConfig) && subBlocks?.inputFormat?.value) { - let inputFormatValue = subBlocks.inputFormat.value - - // Sanitize inputFormat - ensure it's an array - if ( - inputFormatValue !== null && - inputFormatValue !== undefined && - !Array.isArray(inputFormatValue) - ) { - // Invalid format, default to empty array - inputFormatValue = [] - } + const normalizedInputFormat = normalizeInputFormatValue(subBlocks.inputFormat.value) - if (Array.isArray(inputFormatValue)) { - // For API, Input triggers, and Generic Webhook, use inputFormat fields + if (!Array.isArray(subBlocks.inputFormat.value)) { + if (blockType === 'api_trigger' || blockType === 'input_trigger') { + outputs = {} + } + } else { if ( blockType === 'api_trigger' || blockType === 'input_trigger' || blockType === 'generic_webhook' ) { - // For generic_webhook, only clear outputs if inputFormat has fields - // Otherwise keep the default outputs (pass-through body) - if (inputFormatValue.length > 0 || blockType !== 'generic_webhook') { - outputs = {} // Clear all default outputs + if (normalizedInputFormat.length > 0 || blockType !== 'generic_webhook') { + outputs = {} } - - // Add each field from inputFormat as an output at root level - inputFormatValue.forEach((field: { name?: string; type?: string }) => { - if (field?.name && field.name.trim() !== '') { - outputs[field.name] = { - type: (field.type || 'any') as any, - description: `Field from input format`, - } - } - }) + outputs = applyInputFormatFields(normalizedInputFormat, outputs) } - } else if (blockType === 'api_trigger' || blockType === 'input_trigger') { + } + + if ( + !Array.isArray(subBlocks.inputFormat.value) && + (blockType === 'api_trigger' || blockType === 'input_trigger') + ) { // If no inputFormat defined, API/Input trigger has no outputs outputs = {} } @@ -142,6 +163,15 @@ export function getBlockOutputPaths( for (const [key, value] of Object.entries(obj)) { const path = prefix ? `${prefix}.${key}` : key + // For start_trigger, skip reserved fields at root level (they're always present but hidden from dropdown) + if ( + blockType === 'start_trigger' && + !prefix && + ['input', 'conversationId', 'files'].includes(key) + ) { + continue + } + // If value has 'type' property, it's a leaf node (output definition) if (value && typeof value === 'object' && 'type' in value) { // Special handling for 'files' type - expand to show array element properties diff --git a/apps/sim/lib/workflows/triggers.ts b/apps/sim/lib/workflows/triggers.ts index c2f5501107..0b1f73b201 100644 --- a/apps/sim/lib/workflows/triggers.ts +++ b/apps/sim/lib/workflows/triggers.ts @@ -10,6 +10,7 @@ export const TRIGGER_TYPES = { API: 'api_trigger', WEBHOOK: 'webhook', SCHEDULE: 'schedule', + START: 'start_trigger', STARTER: 'starter', // Legacy } as const @@ -20,10 +21,10 @@ export type TriggerType = (typeof TRIGGER_TYPES)[keyof typeof TRIGGER_TYPES] * to concrete trigger block type identifiers used across the system. */ export const TRIGGER_REFERENCE_ALIAS_MAP = { - start: TRIGGER_TYPES.STARTER, + start: TRIGGER_TYPES.START, api: TRIGGER_TYPES.API, chat: TRIGGER_TYPES.CHAT, - manual: TRIGGER_TYPES.INPUT, + manual: TRIGGER_TYPES.START, } as const export type TriggerReferenceAlias = keyof typeof TRIGGER_REFERENCE_ALIAS_MAP @@ -66,7 +67,7 @@ export class TriggerUtils { * Check if a block is a chat-compatible trigger */ static isChatTrigger(block: { type: string; subBlocks?: any }): boolean { - if (block.type === TRIGGER_TYPES.CHAT) { + if (block.type === TRIGGER_TYPES.CHAT || block.type === TRIGGER_TYPES.START) { return true } @@ -82,7 +83,11 @@ export class TriggerUtils { * Check if a block is a manual-compatible trigger */ static isManualTrigger(block: { type: string; subBlocks?: any }): boolean { - if (block.type === TRIGGER_TYPES.INPUT || block.type === TRIGGER_TYPES.MANUAL) { + if ( + block.type === TRIGGER_TYPES.INPUT || + block.type === TRIGGER_TYPES.MANUAL || + block.type === TRIGGER_TYPES.START + ) { return true } @@ -103,11 +108,11 @@ export class TriggerUtils { */ static isApiTrigger(block: { type: string; subBlocks?: any }, isChildWorkflow = false): boolean { if (isChildWorkflow) { - // Child workflows (workflow-in-workflow) only work with input_trigger - return block.type === TRIGGER_TYPES.INPUT + // Child workflows (workflow-in-workflow) support legacy input trigger and new start block + return block.type === TRIGGER_TYPES.INPUT || block.type === TRIGGER_TYPES.START } - // Direct API calls only work with api_trigger - if (block.type === TRIGGER_TYPES.API) { + // Direct API calls work with api_trigger and the new start block + if (block.type === TRIGGER_TYPES.API || block.type === TRIGGER_TYPES.START) { return true } @@ -144,6 +149,8 @@ export class TriggerUtils { return 'Manual' case TRIGGER_TYPES.API: return 'API' + case TRIGGER_TYPES.START: + return 'Start' case TRIGGER_TYPES.WEBHOOK: return 'Webhook' case TRIGGER_TYPES.SCHEDULE: @@ -227,7 +234,8 @@ export class TriggerUtils { triggerType === TRIGGER_TYPES.API || triggerType === TRIGGER_TYPES.INPUT || triggerType === TRIGGER_TYPES.MANUAL || - triggerType === TRIGGER_TYPES.CHAT + triggerType === TRIGGER_TYPES.CHAT || + triggerType === TRIGGER_TYPES.START ) } @@ -255,7 +263,8 @@ export class TriggerUtils { triggerType === TRIGGER_TYPES.CHAT || triggerType === TRIGGER_TYPES.INPUT || triggerType === TRIGGER_TYPES.MANUAL || - triggerType === TRIGGER_TYPES.API + triggerType === TRIGGER_TYPES.API || + triggerType === TRIGGER_TYPES.START ) { return true } @@ -267,13 +276,27 @@ export class TriggerUtils { block.type === TRIGGER_TYPES.CHAT || block.type === TRIGGER_TYPES.INPUT || block.type === TRIGGER_TYPES.MANUAL || - block.type === TRIGGER_TYPES.API + block.type === TRIGGER_TYPES.API || + block.type === TRIGGER_TYPES.START ) if (hasModernTriggers) { return true } } + // Start trigger cannot coexist with other single-instance trigger types + if (triggerType === TRIGGER_TYPES.START) { + return blockArray.some((block) => + [ + TRIGGER_TYPES.START, + TRIGGER_TYPES.API, + TRIGGER_TYPES.INPUT, + TRIGGER_TYPES.MANUAL, + TRIGGER_TYPES.CHAT, + ].includes(block.type as TriggerType) + ) + } + // Only one Input trigger allowed if (triggerType === TRIGGER_TYPES.INPUT) { return blockArray.some((block) => block.type === TRIGGER_TYPES.INPUT) From 2673aeddcbd7d2abc2e03f877f71d281617ac9e8 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 27 Oct 2025 21:22:13 -0700 Subject: [PATCH 3/8] make it work with start block --- .../input-mapping/input-mapping.tsx | 9 ++--- apps/sim/blocks/blocks/workflow_input.ts | 10 +++--- apps/sim/executor/index.ts | 8 +++-- apps/sim/stores/workflows/registry/store.ts | 35 ++++++++++++++++--- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx index 1b48f6f005..cde7cfc002 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx @@ -14,7 +14,7 @@ interface InputFormatField { } interface InputTriggerBlock { - type: 'input_trigger' + type: 'input_trigger' | 'start_trigger' subBlocks?: { inputFormat?: { value?: InputFormatField[] } } @@ -33,8 +33,9 @@ interface StarterBlockLegacy { } function isInputTriggerBlock(value: unknown): value is InputTriggerBlock { + const type = (value as { type?: unknown }).type return ( - !!value && typeof value === 'object' && (value as { type?: unknown }).type === 'input_trigger' + !!value && typeof value === 'object' && (type === 'input_trigger' || type === 'start_trigger') ) } @@ -74,7 +75,7 @@ export function InputMapping({ // 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. + // We only need the inputFormat from a Start or Input Trigger in the selected child workflow state. const [childInputFields, setChildInputFields] = useState>( [] ) @@ -97,7 +98,7 @@ export function InputMapping({ } const { data } = await res.json() const blocks = (data?.state?.blocks as Record) || {} - // Prefer new input_trigger + // Prefer start_trigger or input_trigger const triggerEntry = Object.entries(blocks).find(([, b]) => isInputTriggerBlock(b)) if (triggerEntry && isInputTriggerBlock(triggerEntry[1])) { const inputFormat = triggerEntry[1].subBlocks?.inputFormat?.value diff --git a/apps/sim/blocks/blocks/workflow_input.ts b/apps/sim/blocks/blocks/workflow_input.ts index de5d6014f7..244922308e 100644 --- a/apps/sim/blocks/blocks/workflow_input.ts +++ b/apps/sim/blocks/blocks/workflow_input.ts @@ -19,11 +19,11 @@ const getAvailableWorkflows = (): Array<{ label: string; id: string }> => { export const WorkflowInputBlock: BlockConfig = { type: 'workflow_input', name: 'Workflow', - description: 'Execute another workflow and map variables to its Input Form Trigger schema.', - longDescription: `Execute another child workflow and map variables to its Input Form Trigger schema. Helps with modularizing workflows.`, + description: 'Execute another workflow and map variables to its Start trigger schema.', + longDescription: `Execute another child workflow and map variables to its Start trigger schema. Helps with modularizing workflows.`, bestPractices: ` - 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. - - Remember, that the start point of the child workflow is the Input Form Trigger block. + - Remember, that the start point of the child workflow is the Start block. `, category: 'blocks', bgColor: '#6366F1', // Indigo - modern and professional @@ -36,14 +36,14 @@ export const WorkflowInputBlock: BlockConfig = { options: getAvailableWorkflows, required: true, }, - // Renders dynamic mapping UI based on selected child workflow's Input Trigger inputFormat + // Renders dynamic mapping UI based on selected child workflow's Start 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.", + "Map fields defined in the child workflow's Start block to variables/values in this workflow.", dependsOn: ['workflowId'], }, ], diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index 1af8af8238..5f5885690d 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -783,17 +783,21 @@ export class Executor { if (!initBlock) { if (this.isChildExecution) { const inputTriggerBlocks = this.actualWorkflow.blocks.filter( - (block) => block.metadata?.id === 'input_trigger' + (block) => + block.metadata?.id === 'input_trigger' || block.metadata?.id === 'start_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.') + throw new Error( + 'Child workflow has multiple trigger blocks. Keep only one Start block.' + ) } } else { // Parent workflows can use any trigger block (dedicated or trigger-mode) const triggerBlocks = this.actualWorkflow.blocks.filter( (block) => + block.metadata?.id === 'start_trigger' || block.metadata?.id === 'input_trigger' || block.metadata?.id === 'api_trigger' || block.metadata?.id === 'chat_trigger' || diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index e83dd0bd4a..dff0d18004 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -442,9 +442,36 @@ export const useWorkflowRegistry = create()( deploymentStatuses: {}, } } else { - // If no state in DB, use empty state - server should have created start block + // If no state in DB, create a default Start block + const starterId = crypto.randomUUID() workflowState = { - blocks: {}, + blocks: { + [starterId]: { + id: starterId, + type: 'start_trigger', + name: 'Start', + position: { x: 200, y: 300 }, + enabled: true, + horizontalHandles: true, + isWide: false, + advancedMode: false, + triggerMode: false, + height: 0, + subBlocks: { + inputFormat: { + id: 'inputFormat', + type: 'input-format', + value: [], + }, + }, + outputs: { + input: { type: 'string', description: 'Primary user input or message' }, + conversationId: { type: 'string', description: 'Conversation thread identifier' }, + files: { type: 'files', description: 'User uploaded files' }, + }, + data: {}, + }, + }, edges: [], loops: {}, parallels: {}, @@ -454,9 +481,7 @@ export const useWorkflowRegistry = create()( lastSaved: Date.now(), } - logger.warn( - `Workflow ${id} has no state in DB - this should not happen with server-side start block creation` - ) + logger.info(`Created default Start block for empty workflow ${id}`) } if (workflowData?.isDeployed || workflowData?.deployedAt) { From ba374ec3a92a904a6a38faf6cbe07411d906e88e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 27 Oct 2025 21:32:27 -0700 Subject: [PATCH 4/8] fix start block persistence --- apps/sim/stores/workflows/registry/store.ts | 101 +++++++++++++------- 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index dff0d18004..3dac58842c 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -442,36 +442,9 @@ export const useWorkflowRegistry = create()( deploymentStatuses: {}, } } else { - // If no state in DB, create a default Start block - const starterId = crypto.randomUUID() + // If no state in DB, use empty state (Start block was created during workflow creation) workflowState = { - blocks: { - [starterId]: { - id: starterId, - type: 'start_trigger', - name: 'Start', - position: { x: 200, y: 300 }, - enabled: true, - horizontalHandles: true, - isWide: false, - advancedMode: false, - triggerMode: false, - height: 0, - subBlocks: { - inputFormat: { - id: 'inputFormat', - type: 'input-format', - value: [], - }, - }, - outputs: { - input: { type: 'string', description: 'Primary user input or message' }, - conversationId: { type: 'string', description: 'Conversation thread identifier' }, - files: { type: 'files', description: 'User uploaded files' }, - }, - data: {}, - }, - }, + blocks: {}, edges: [], loops: {}, parallels: {}, @@ -481,7 +454,7 @@ export const useWorkflowRegistry = create()( lastSaved: Date.now(), } - logger.info(`Created default Start block for empty workflow ${id}`) + logger.info(`Workflow ${id} has no state yet - will load from DB or show empty canvas`) } if (workflowData?.isDeployed || workflowData?.deployedAt) { @@ -591,16 +564,76 @@ export const useWorkflowRegistry = create()( // Initialize subblock values to ensure they're available for sync if (!options.marketplaceId) { - // For non-marketplace workflows, initialize empty subblock values - const subblockValues: Record> = {} + // For non-marketplace workflows, create a default Start block + const starterId = crypto.randomUUID() + const defaultStartBlock = { + id: starterId, + type: 'start_trigger', + name: 'Start', + position: { x: 200, y: 300 }, + enabled: true, + horizontalHandles: true, + isWide: false, + advancedMode: false, + triggerMode: false, + height: 0, + subBlocks: { + inputFormat: { + id: 'inputFormat', + type: 'input-format', + value: [], + }, + }, + outputs: { + input: { type: 'string', description: 'Primary user input or message' }, + conversationId: { type: 'string', description: 'Conversation thread identifier' }, + files: { type: 'files', description: 'User uploaded files' }, + }, + data: {}, + } - // Update the subblock store with the initial values + // Initialize subblock values useSubBlockStore.setState((state) => ({ workflowValues: { ...state.workflowValues, - [serverWorkflowId]: subblockValues, + [serverWorkflowId]: { + [starterId]: { + inputFormat: [], + }, + }, }, })) + + // Persist the default Start block to database immediately + + ;(async () => { + try { + logger.info(`Persisting default Start block for new workflow ${serverWorkflowId}`) + const response = await fetch(`/api/workflows/${serverWorkflowId}/state`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + blocks: { + [starterId]: defaultStartBlock, + }, + edges: [], + loops: {}, + parallels: {}, + lastSaved: Date.now(), + }), + }) + + if (!response.ok) { + logger.error('Failed to persist default Start block:', await response.text()) + } else { + logger.info('Successfully persisted default Start block') + } + } catch (error) { + logger.error('Error persisting default Start block:', error) + } + })() } // Don't set as active workflow here - let the navigation/URL change handle that From 4590b5c0ccd969ddaf3bfc9719c0bd29ef769f58 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 28 Oct 2025 16:28:10 -0700 Subject: [PATCH 5/8] cleanup triggers --- .../app/api/workflows/[id]/execute/route.ts | 7 +- apps/sim/app/api/workspaces/route.ts | 9 + .../components/deploy-modal/deploy-modal.tsx | 15 +- .../hooks/use-workflow-execution.ts | 168 ++---- apps/sim/executor/index.ts | 537 ++++++++---------- apps/sim/executor/resolver/resolver.ts | 40 +- apps/sim/executor/utils/start-block.test.ts | 125 ++++ apps/sim/executor/utils/start-block.ts | 406 +++++++++++++ apps/sim/lib/workflows/block-outputs.ts | 7 +- apps/sim/lib/workflows/defaults.ts | 125 ++++ apps/sim/lib/workflows/triggers.ts | 214 ++++++- apps/sim/stores/workflows/registry/store.ts | 186 +----- 12 files changed, 1226 insertions(+), 613 deletions(-) create mode 100644 apps/sim/executor/utils/start-block.test.ts create mode 100644 apps/sim/executor/utils/start-block.ts create mode 100644 apps/sim/lib/workflows/defaults.ts diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 0f67436d25..da98213640 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -14,7 +14,7 @@ import { LoggingSession } from '@/lib/logs/execution/logging-session' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' import { decryptSecret, generateRequestId } from '@/lib/utils' import { loadDeployedWorkflowState } from '@/lib/workflows/db-helpers' -import { TriggerUtils } from '@/lib/workflows/triggers' +import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers' import { createHttpResponseFromBlock, updateWorkflowRunCounts, @@ -313,10 +313,9 @@ export async function executeWorkflow( throw new Error(errorMsg) } - const startBlockId = startBlock.blockId - const triggerBlock = startBlock.block + const { blockId: startBlockId, block: triggerBlock, path: startPath } = startBlock - if (triggerBlock.type !== 'starter') { + if (startPath !== StartBlockPath.LEGACY_STARTER) { const outgoingConnections = serializedWorkflow.connections.filter( (conn) => conn.source === startBlockId ) diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index 736256d1e3..b007942aba 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -4,6 +4,8 @@ import { and, desc, eq, isNull } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' +import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers' +import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' const logger = createLogger('Workspaces') @@ -137,6 +139,13 @@ async function createWorkspace(userId: string, name: string) { `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}` ) }) + + const { workflowState } = buildDefaultWorkflowArtifacts() + const seedResult = await saveWorkflowToNormalizedTables(workflowId, workflowState) + + if (!seedResult.success) { + throw new Error(seedResult.error || 'Failed to seed default workflow state') + } } catch (error) { logger.error(`Failed to create workspace ${workspaceId} with initial workflow:`, error) throw error diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx index 00eb9d679a..b1a52d9253 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx @@ -17,6 +17,7 @@ import { getEnv } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/db-helpers' +import { resolveStartCandidates, StartBlockPath } from '@/lib/workflows/triggers' import { DeployForm, DeploymentInfo, @@ -120,13 +121,17 @@ export function DeployModal({ let inputFormatExample = '' try { const blocks = Object.values(useWorkflowStore.getState().blocks) + const candidates = resolveStartCandidates(useWorkflowStore.getState().blocks, { + execution: 'api', + }) - // Check for start_trigger first, then API trigger, then legacy starter - const startTriggerBlock = blocks.find((block) => block.type === 'start_trigger') - const apiTriggerBlock = blocks.find((block) => block.type === 'api_trigger') - const starterBlock = blocks.find((block) => block.type === 'starter') + const targetCandidate = + candidates.find((candidate) => candidate.path === StartBlockPath.UNIFIED) || + candidates.find((candidate) => candidate.path === StartBlockPath.SPLIT_API) || + candidates.find((candidate) => candidate.path === StartBlockPath.SPLIT_INPUT) || + candidates.find((candidate) => candidate.path === StartBlockPath.LEGACY_STARTER) - const targetBlock = startTriggerBlock || apiTriggerBlock || starterBlock + const targetBlock = targetCandidate?.block if (targetBlock) { const inputFormat = useSubBlockStore.getState().getValue(targetBlock.id, 'inputFormat') diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 4f77624b93..8514218b1b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid' import { createLogger } from '@/lib/logs/console/logger' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' import { processStreamingBlockLogs } from '@/lib/tokenization' -import { TriggerUtils } from '@/lib/workflows/triggers' +import { resolveStartCandidates, StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers' import type { BlockOutput } from '@/blocks/types' import { Executor } from '@/executor' import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types' @@ -824,132 +824,76 @@ export function useWorkflowExecution() { 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', + const candidates = resolveStartCandidates(filteredStates, { + execution: 'manual', + }) + + logger.info('Manual run start candidates:', { + count: candidates.length, + paths: candidates.map((candidate) => ({ + path: candidate.path, + type: candidate.block.type, + name: candidate.block.name, })), }) - 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 === 'start_trigger' || - selectedTrigger.type === 'starter' - ) { - const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value - const testInput = extractTestValuesFromInputFormat(inputFormatValue) - - // Use the test input as workflow input - if (Object.keys(testInput).length > 0) { - finalWorkflowInput = testInput - logger.info('Using trigger test values for manual run:', testInput) - } - } - } - } else if (apiTriggers.length > 1) { + const apiCandidates = candidates.filter( + (candidate) => candidate.path === StartBlockPath.SPLIT_API + ) + if (apiCandidates.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 triggers - // Prefer manual_trigger over input_trigger for simple runs - const manualTrigger = manualTriggers.find((t) => t.type === 'manual_trigger') - const inputTrigger = manualTriggers.find((t) => t.type === 'input_trigger') - - selectedTrigger = manualTrigger || inputTrigger || manualTriggers[0] - const blockEntry = entries.find(([, block]) => block === selectedTrigger) - if (blockEntry) { - selectedBlockId = blockEntry[0] - - // Extract test values from input trigger's inputFormat if it's an input_trigger or start_trigger - if ( - selectedTrigger.type === 'input_trigger' || - selectedTrigger.type === 'start_trigger' - ) { - const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value - const testInput = extractTestValuesFromInputFormat(inputFormatValue) - - // Use the test input as workflow input - if (Object.keys(testInput).length > 0) { - finalWorkflowInput = testInput - logger.info('Using trigger test values for manual run:', testInput) - } - } - } - } 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 a Manual, Input Form, or API Trigger block') - logger.error('No manual/input or API triggers found for manual run') + const selectedCandidate = apiCandidates[0] ?? candidates[0] + + if (!selectedCandidate) { + const error = new Error('Manual run requires a Manual, Input Form, or API Trigger block') + logger.error('No manual/input or API triggers found for manual run') + setIsExecuting(false) + throw error + } + + startBlockId = selectedCandidate.blockId + const selectedTrigger = selectedCandidate.block + + if (selectedCandidate.path !== StartBlockPath.LEGACY_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 } } - 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 - } + if ( + selectedCandidate.path === StartBlockPath.SPLIT_API || + selectedCandidate.path === StartBlockPath.SPLIT_INPUT || + selectedCandidate.path === StartBlockPath.UNIFIED + ) { + const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value + const testInput = extractTestValuesFromInputFormat(inputFormatValue) + + if (Object.keys(testInput).length > 0) { + finalWorkflowInput = testInput + logger.info('Using trigger test values for manual run:', { + startBlockId, + testFields: Object.keys(testInput), + path: selectedCandidate.path, + }) } - - logger.info('Trigger found for manual run:', { - startBlockId, - triggerType: selectedTrigger.type, - triggerName: selectedTrigger.name, - isLegacyStarter: selectedTrigger.type === 'starter', - usingTestValues: selectedTrigger.type === 'api_trigger', - }) } + + logger.info('Trigger found for manual run:', { + startBlockId, + triggerType: selectedTrigger.type, + triggerName: selectedTrigger.name, + startPath: selectedCandidate.path, + }) } // If we don't have a valid startBlockId at this point, throw an error diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index 5f5885690d..ff043310e1 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -32,6 +32,12 @@ import type { StreamingExecution, } from '@/executor/types' import { streamingResponseFormatProcessor } from '@/executor/utils' +import { + buildResolutionFromBlock, + buildStartBlockOutput, + type ExecutorStartResolution, + resolveExecutorStartBlock, +} from '@/executor/utils/start-block' import { VirtualBlockUtils } from '@/executor/utils/virtual-blocks' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' import { useExecutionStore } from '@/stores/execution/store' @@ -770,346 +776,277 @@ export class Executor { // Determine which block to initialize as the starting point let initBlock: SerializedBlock | undefined + let startResolution: ExecutorStartResolution | null = null + if (startBlockId) { - // Starting from a specific block (webhook trigger, schedule trigger, or new trigger blocks) initBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId) + if (initBlock) { + startResolution = buildResolutionFromBlock(initBlock) + } } else { - // Default to starter block (legacy) or find any trigger block - initBlock = this.actualWorkflow.blocks.find( - (block) => block.metadata?.id === BlockType.STARTER - ) + const executionKind = this.getExecutionKindHint() + startResolution = resolveExecutorStartBlock(this.actualWorkflow.blocks, { + execution: executionKind, + isChildWorkflow: this.isChildExecution, + }) - // 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' || block.metadata?.id === 'start_trigger' - ) - if (inputTriggerBlocks.length === 1) { - initBlock = inputTriggerBlocks[0] - } else if (inputTriggerBlocks.length > 1) { - throw new Error( - 'Child workflow has multiple trigger blocks. Keep only one Start block.' - ) - } - } else { - // Parent workflows can use any trigger block (dedicated or trigger-mode) - const triggerBlocks = this.actualWorkflow.blocks.filter( - (block) => - block.metadata?.id === 'start_trigger' || - 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 (startResolution) { + initBlock = startResolution.block + } else { + initBlock = this.resolveFallbackStartBlock() } } if (initBlock) { - // Initialize the starting block with the workflow input try { - // Get inputFormat from either old location (config.params) or new location (metadata.subBlocks) - const blockParams = initBlock.config.params - 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 + const resolution = startResolution ?? buildResolutionFromBlock(initBlock) + + if (resolution) { + const blockOutput = buildStartBlockOutput({ + resolution, + workflowInput: this.workflowInput, + isDeployedExecution: this.contextExtensions?.isDeployedContext === true, + }) + + context.blockStates.set(initBlock.id, { + output: blockOutput, + executed: true, + executionTime: 0, + }) + + this.createStartedBlockWithFilesLog(initBlock, blockOutput, context) + } else { + this.initializeLegacyTriggerBlock(initBlock, context) } + } catch (error) { + logger.warn('Error processing starter block input format:', error) - // If input format is defined, structure the input according to the schema - if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) { - // Create structured input based on input format - const structuredInput: Record = {} - - // Process each field in the input format - for (const field of inputFormat) { - if (field.name && field.type) { - // Get the field value from workflow input if available - // First try to access via input.field, then directly from field - // This handles both input formats: { input: { field: value } } and { field: value } - let inputValue = - this.workflowInput?.input?.[field.name] !== undefined - ? this.workflowInput.input[field.name] // Try to get from input.field - : this.workflowInput?.[field.name] // Fallback to direct field access - - // Only use test values for manual editor runs, not for deployed executions (API/webhook/schedule/chat) - const isDeployedExecution = this.contextExtensions?.isDeployedContext === true - if (inputValue === undefined || inputValue === null) { - if (!isDeployedExecution && Object.hasOwn(field, 'value')) { - inputValue = (field as any).value - } - } + const blockOutput = this.buildFallbackTriggerOutput(initBlock) + context.blockStates.set(initBlock.id, { + output: blockOutput, + executed: true, + executionTime: 0, + }) + this.createStartedBlockWithFilesLog(initBlock, blockOutput, context) + } - let typedValue = inputValue - if (inputValue !== undefined && inputValue !== null) { - if (field.type === 'string' && typeof inputValue !== 'string') { - typedValue = String(inputValue) - } else if (field.type === 'number' && typeof inputValue !== 'number') { - const num = Number(inputValue) - typedValue = Number.isNaN(num) ? inputValue : num - } else if (field.type === 'boolean' && typeof inputValue !== 'boolean') { - typedValue = - inputValue === 'true' || - inputValue === true || - inputValue === 1 || - inputValue === '1' - } else if ( - (field.type === 'object' || field.type === 'array') && - typeof inputValue === 'string' - ) { - try { - typedValue = JSON.parse(inputValue) - } catch (e) { - logger.warn(`Failed to parse ${field.type} input for field ${field.name}:`, e) - } - } - } + context.activeExecutionPath.add(initBlock.id) + context.executedBlocks.add(initBlock.id) + this.addConnectedBlocksToActivePath(initBlock.id, context) + } - // Add the field to structured input - structuredInput[field.name] = typedValue - } - } + return context + } - // Check if we managed to process any fields - if not, use the raw input - const hasProcessedFields = Object.keys(structuredInput).length > 0 + private getExecutionKindHint(): 'chat' | 'manual' | 'api' { + const triggerType = this.contextExtensions?.workflowTriggerType + if (triggerType === 'chat') { + return 'chat' + } + if (triggerType === 'api') { + return 'api' + } + if (this.contextExtensions?.stream === true) { + return 'chat' + } + if (this.contextExtensions?.isDeployedContext === true) { + return 'api' + } + return 'manual' + } - // If no fields matched the input format, extract the raw input to use instead - const rawInputData = - this.workflowInput?.input !== undefined - ? this.workflowInput.input // Use the input value - : this.workflowInput // Fallback to direct input + private resolveFallbackStartBlock(): SerializedBlock | undefined { + if (this.isChildExecution) { + return undefined + } - // Use the structured input if we processed fields, otherwise use raw input - const finalInput = hasProcessedFields ? structuredInput : rawInputData + const excluded = new Set([ + 'start_trigger', + 'input_trigger', + 'api_trigger', + 'chat_trigger', + 'starter', + ]) + + const triggerBlocks = this.actualWorkflow.blocks.filter( + (block) => + block.metadata?.category === 'triggers' || block.config?.params?.triggerMode === true + ) - // Initialize the starting block with structured input - let blockOutput: any + return ( + triggerBlocks.find((block) => !excluded.has(block.metadata?.id ?? '')) || triggerBlocks[0] + ) + } - // For Start/API/Input triggers, normalize primitives and mirror objects under input - const isStartTrigger = initBlock.metadata?.id === 'start_trigger' - if (isStartTrigger) { - // Start trigger: pass through entire workflowInput payload + set reserved fields - blockOutput = {} + private initializeLegacyTriggerBlock( + initBlock: SerializedBlock, + context: ExecutionContext + ): void { + const blockParams = initBlock.config.params + let inputFormat = blockParams?.inputFormat + const metadataWithSubBlocks = initBlock.metadata as { + subBlocks?: Record + } | null + + if (!inputFormat && metadataWithSubBlocks?.subBlocks?.inputFormat?.value) { + inputFormat = metadataWithSubBlocks.subBlocks.inputFormat.value + } - // First, spread the entire workflowInput (all fields from API payload) - if (this.workflowInput && typeof this.workflowInput === 'object') { - for (const [key, value] of Object.entries(this.workflowInput)) { - if (key !== 'onUploadError') { - blockOutput[key] = value - } - } - } + if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) { + const structuredInput: Record = {} + const isDeployedExecution = this.contextExtensions?.isDeployedContext === true - // Then ensure reserved fields are always set correctly for chat compatibility - if (!blockOutput.input) { - blockOutput.input = '' - } - if (!blockOutput.conversationId) { - blockOutput.conversationId = '' - } - } else 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 / legacy references - blockOutput.input = { ...finalInput } - } else { - // Primitive input: only expose under input - blockOutput = { input: finalInput } - } + for (const field of inputFormat) { + if (field?.name && field.type) { + let inputValue = + this.workflowInput?.input?.[field.name] !== undefined + ? this.workflowInput.input[field.name] + : this.workflowInput?.[field.name] - // Add files if present (for all trigger types) - if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) { - blockOutput.files = this.workflowInput.files - } - } 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 + if ((inputValue === undefined || inputValue === null) && !isDeployedExecution) { + if (Object.hasOwn(field, 'value')) { + inputValue = (field as any).value } + } - // Add files if present (for all trigger types) - if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) { - blockOutput.files = this.workflowInput.files + let typedValue = inputValue + if (inputValue !== undefined && inputValue !== null) { + if (field.type === 'string' && typeof inputValue !== 'string') { + typedValue = String(inputValue) + } else if (field.type === 'number' && typeof inputValue !== 'number') { + const num = Number(inputValue) + typedValue = Number.isNaN(num) ? inputValue : num + } else if (field.type === 'boolean' && typeof inputValue !== 'boolean') { + typedValue = + inputValue === 'true' || + inputValue === true || + inputValue === 1 || + inputValue === '1' + } else if ( + (field.type === 'object' || field.type === 'array') && + typeof inputValue === 'string' + ) { + try { + typedValue = JSON.parse(inputValue) + } catch (error) { + logger.warn(`Failed to parse ${field.type} input for field ${field.name}:`, error) + } } } - context.blockStates.set(initBlock.id, { - output: blockOutput, - executed: true, - executionTime: 0, - }) + structuredInput[field.name] = typedValue + } + } - // 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, blockOutput, context) - } else { - // Handle triggers without inputFormat - let starterOutput: any - - // Handle different trigger types - if (initBlock.metadata?.id === 'start_trigger') { - // Start trigger without inputFormat: pass through entire payload + ensure reserved fields - starterOutput = {} - - // Pass through entire workflowInput - if (this.workflowInput && typeof this.workflowInput === 'object') { - for (const [key, value] of Object.entries(this.workflowInput)) { - if (key !== 'onUploadError') { - starterOutput[key] = value - } - } - } + const hasProcessedFields = Object.keys(structuredInput).length > 0 + const rawInputData = + this.workflowInput?.input !== undefined ? this.workflowInput.input : this.workflowInput + const finalInput = hasProcessedFields ? structuredInput : rawInputData - // Ensure reserved fields are always set - if (!starterOutput.input) { - starterOutput.input = '' - } - if (!starterOutput.conversationId) { - starterOutput.conversationId = '' - } - } else if (initBlock.metadata?.id === 'chat_trigger') { - // Chat trigger: extract input, conversationId, and files - starterOutput = { - input: this.workflowInput?.input || '', - conversationId: this.workflowInput?.conversationId || '', - } + let blockOutput: NormalizedBlockOutput + if (hasProcessedFields) { + blockOutput = { + ...(structuredInput as Record), + input: structuredInput, + conversationId: this.workflowInput?.conversationId, + } + } else if ( + finalInput !== null && + typeof finalInput === 'object' && + !Array.isArray(finalInput) + ) { + blockOutput = { + ...(finalInput as Record), + input: finalInput, + conversationId: this.workflowInput?.conversationId, + } + } else { + blockOutput = { + input: finalInput, + 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), - input: { ...(rawCandidate as Record) }, - } - } else { - starterOutput = { input: rawCandidate } - } - } else { - // Legacy starter block handling - if (this.workflowInput && typeof this.workflowInput === 'object') { - // 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, - } + if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) { + blockOutput.files = this.workflowInput.files + } - // Add files if present - if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) { - starterOutput.files = this.workflowInput.files - } - } 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: blockOutput, + executed: true, + executionTime: 0, + }) + this.createStartedBlockWithFilesLog(initBlock, blockOutput, context) + return + } - context.blockStates.set(initBlock.id, { - output: starterOutput, - executed: true, - executionTime: 0, - }) + let starterOutput: NormalizedBlockOutput - // 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) - } + if (this.workflowInput && typeof this.workflowInput === 'object') { + if ( + Object.hasOwn(this.workflowInput, 'input') && + Object.hasOwn(this.workflowInput, 'conversationId') + ) { + starterOutput = { + input: this.workflowInput.input, + conversationId: this.workflowInput.conversationId, } - } catch (e) { - logger.warn('Error processing starter block input format:', e) - - // Error handler fallback - use appropriate structure - let blockOutput: any - if (this.workflowInput && typeof this.workflowInput === 'object') { - // 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 - blockOutput = { - input: this.workflowInput.input, - conversationId: this.workflowInput.conversationId, - } - // Add files if present - if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) { - blockOutput.files = this.workflowInput.files - } - } else { - // API workflow: spread the raw data directly (no wrapping) - blockOutput = { ...this.workflowInput } - } - } else { - // Primitive input - blockOutput = { - input: this.workflowInput, - } + if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) { + starterOutput.files = this.workflowInput.files } - - context.blockStates.set(initBlock.id, { - output: blockOutput, - executed: true, - executionTime: 0, - }) - this.createStartedBlockWithFilesLog(initBlock, blockOutput, context) + } else { + starterOutput = { ...this.workflowInput } } - // Ensure the starting block is in the active execution path - context.activeExecutionPath.add(initBlock.id) - // Mark the starting block as executed - context.executedBlocks.add(initBlock.id) + } else { + starterOutput = { + input: this.workflowInput, + } + } - // Add all blocks connected to the starting block to the active execution path - const connectedToStartBlock = this.actualWorkflow.connections - .filter((conn) => conn.source === initBlock.id) - .map((conn) => conn.target) + context.blockStates.set(initBlock.id, { + output: starterOutput, + executed: true, + executionTime: 0, + }) - connectedToStartBlock.forEach((blockId) => { - context.activeExecutionPath.add(blockId) - }) + if (starterOutput.files) { + this.createStartedBlockWithFilesLog(initBlock, starterOutput, context) } + } - return context + private buildFallbackTriggerOutput(_: SerializedBlock): NormalizedBlockOutput { + if (this.workflowInput && typeof this.workflowInput === 'object') { + if ( + Object.hasOwn(this.workflowInput, 'input') && + Object.hasOwn(this.workflowInput, 'conversationId') + ) { + const output: NormalizedBlockOutput = { + input: this.workflowInput.input, + conversationId: this.workflowInput.conversationId, + } + + if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) { + output.files = this.workflowInput.files + } + + return output + } + + return { ...(this.workflowInput as Record) } as NormalizedBlockOutput + } + + return { input: this.workflowInput } as NormalizedBlockOutput + } + + private addConnectedBlocksToActivePath(sourceBlockId: string, context: ExecutionContext): void { + const connectedToSource = this.actualWorkflow.connections + .filter((conn) => conn.source === sourceBlockId) + .map((conn) => conn.target) + + connectedToSource.forEach((blockId) => { + context.activeExecutionPath.add(blockId) + }) } /** diff --git a/apps/sim/executor/resolver/resolver.ts b/apps/sim/executor/resolver/resolver.ts index 4bebd6d0fc..5bb77eedf6 100644 --- a/apps/sim/executor/resolver/resolver.ts +++ b/apps/sim/executor/resolver/resolver.ts @@ -1,7 +1,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { VariableManager } from '@/lib/variables/variable-manager' import { extractReferencePrefixes, SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/references' -import { TRIGGER_REFERENCE_ALIAS_MAP } from '@/lib/workflows/triggers' +import { TRIGGER_REFERENCE_ALIAS_MAP, TRIGGER_TYPES } from '@/lib/workflows/triggers' import { getBlock } from '@/blocks/index' import type { LoopManager } from '@/executor/loops/loops' import type { ExecutionContext } from '@/executor/types' @@ -497,9 +497,41 @@ export class InputResolver { const triggerType = TRIGGER_REFERENCE_ALIAS_MAP[blockRefLower as keyof typeof TRIGGER_REFERENCE_ALIAS_MAP] if (triggerType) { - const triggerBlock = this.workflow.blocks.find( - (block) => block.metadata?.id === triggerType - ) + const candidateTypes: string[] = [] + const pushCandidate = (candidate?: string) => { + if (candidate && !candidateTypes.includes(candidate)) { + candidateTypes.push(candidate) + } + } + + pushCandidate(triggerType) + + if (blockRefLower === 'start' || blockRefLower === 'manual') { + pushCandidate(TRIGGER_TYPES.START) + pushCandidate(TRIGGER_TYPES.STARTER) + pushCandidate(TRIGGER_TYPES.INPUT) + pushCandidate(TRIGGER_TYPES.MANUAL) + pushCandidate(TRIGGER_TYPES.API) + pushCandidate(TRIGGER_TYPES.CHAT) + } else if (blockRefLower === 'api') { + pushCandidate(TRIGGER_TYPES.API) + pushCandidate(TRIGGER_TYPES.START) + pushCandidate(TRIGGER_TYPES.INPUT) + pushCandidate(TRIGGER_TYPES.STARTER) + } else if (blockRefLower === 'chat') { + pushCandidate(TRIGGER_TYPES.CHAT) + pushCandidate(TRIGGER_TYPES.START) + pushCandidate(TRIGGER_TYPES.STARTER) + } + + let triggerBlock: SerializedBlock | undefined + for (const candidateType of candidateTypes) { + triggerBlock = this.workflow.blocks.find((block) => block.metadata?.id === candidateType) + if (triggerBlock) { + break + } + } + if (triggerBlock) { const blockState = context.blockStates.get(triggerBlock.id) if (blockState) { diff --git a/apps/sim/executor/utils/start-block.test.ts b/apps/sim/executor/utils/start-block.test.ts new file mode 100644 index 0000000000..11f8a9a8ef --- /dev/null +++ b/apps/sim/executor/utils/start-block.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from 'vitest' +import { StartBlockPath } from '@/lib/workflows/triggers' +import type { UserFile } from '@/executor/types' +import { + buildResolutionFromBlock, + buildStartBlockOutput, + resolveExecutorStartBlock, +} from '@/executor/utils/start-block' +import type { SerializedBlock } from '@/serializer/types' + +function createBlock( + type: string, + id = type, + options?: { subBlocks?: Record } +): SerializedBlock { + return { + id, + position: { x: 0, y: 0 }, + config: { + tool: type, + params: options?.subBlocks?.inputFormat ? { inputFormat: options.subBlocks.inputFormat } : {}, + }, + inputs: {}, + outputs: {}, + metadata: { + id: type, + name: `block-${type}`, + category: 'triggers', + ...(options?.subBlocks ? { subBlocks: options.subBlocks } : {}), + } as SerializedBlock['metadata'] & { subBlocks?: Record }, + enabled: true, + } +} + +describe('start-block utilities', () => { + it('buildResolutionFromBlock returns null when metadata id missing', () => { + const block = createBlock('api_trigger') + ;(block.metadata as Record).id = undefined + + expect(buildResolutionFromBlock(block)).toBeNull() + }) + + it('resolveExecutorStartBlock prefers unified start block', () => { + const blocks = [ + createBlock('api_trigger', 'api'), + createBlock('starter', 'starter'), + createBlock('start_trigger', 'start'), + ] + + const resolution = resolveExecutorStartBlock(blocks, { + execution: 'api', + isChildWorkflow: false, + }) + + expect(resolution?.blockId).toBe('start') + expect(resolution?.path).toBe(StartBlockPath.UNIFIED) + }) + + it('buildStartBlockOutput normalizes unified start payload', () => { + const block = createBlock('start_trigger', 'start') + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.UNIFIED, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { payload: 'value' }, + isDeployedExecution: true, + }) + + expect(output.payload).toBe('value') + expect(output.input).toBe('') + expect(output.conversationId).toBe('') + }) + + it('buildStartBlockOutput uses trigger schema for API triggers', () => { + const apiBlock = createBlock('api_trigger', 'api', { + subBlocks: { + inputFormat: { + value: [ + { name: 'name', type: 'string' }, + { name: 'count', type: 'number' }, + ], + }, + }, + }) + + const resolution = { + blockId: 'api', + block: apiBlock, + path: StartBlockPath.SPLIT_API, + } as const + + const files: UserFile[] = [ + { + id: 'file-1', + name: 'document.txt', + url: 'https://example.com/document.txt', + size: 42, + type: 'text/plain', + key: 'file-key', + uploadedAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 1000).toISOString(), + }, + ] + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { + input: { + name: 'Ada', + count: '5', + }, + files, + }, + isDeployedExecution: false, + }) + + expect(output.name).toBe('Ada') + expect(output.input).toEqual({ name: 'Ada', count: 5 }) + expect(output.files).toEqual(files) + }) +}) diff --git a/apps/sim/executor/utils/start-block.ts b/apps/sim/executor/utils/start-block.ts new file mode 100644 index 0000000000..b42828b0dc --- /dev/null +++ b/apps/sim/executor/utils/start-block.ts @@ -0,0 +1,406 @@ +import { + classifyStartBlockType, + getLegacyStarterMode, + resolveStartCandidates, + StartBlockPath, +} from '@/lib/workflows/triggers' +import type { NormalizedBlockOutput, UserFile } from '@/executor/types' +import type { SerializedBlock } from '@/serializer/types' + +type ExecutionKind = 'chat' | 'manual' | 'api' + +export interface ExecutorStartResolution { + blockId: string + block: SerializedBlock + path: StartBlockPath +} + +export interface ResolveExecutorStartOptions { + execution: ExecutionKind + isChildWorkflow: boolean +} + +type StartCandidateWrapper = { + type: string + subBlocks?: Record + original: SerializedBlock +} + +export function resolveExecutorStartBlock( + blocks: SerializedBlock[], + options: ResolveExecutorStartOptions +): ExecutorStartResolution | null { + if (blocks.length === 0) { + return null + } + + const blockMap = blocks.reduce>((acc, block) => { + const type = block.metadata?.id + if (!type) { + return acc + } + + acc[block.id] = { + type, + subBlocks: extractSubBlocks(block), + original: block, + } + + return acc + }, {}) + + const candidates = resolveStartCandidates(blockMap, { + execution: options.execution, + isChildWorkflow: options.isChildWorkflow, + }) + + if (candidates.length === 0) { + return null + } + + if (options.isChildWorkflow && candidates.length > 1) { + throw new Error('Child workflow has multiple trigger blocks. Keep only one Start block.') + } + + const [primary] = candidates + return { + blockId: primary.blockId, + block: primary.block.original, + path: primary.path, + } +} + +export function buildResolutionFromBlock(block: SerializedBlock): ExecutorStartResolution | null { + const type = block.metadata?.id + if (!type) { + return null + } + + const path = classifyStartBlockType(type) + if (!path) { + return null + } + + return { + blockId: block.id, + block, + path, + } +} + +type InputFormatField = { + name?: string + type?: string | null + value?: unknown +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function readMetadataSubBlockValue(block: SerializedBlock, key: string): unknown { + const metadata = block.metadata + if (!metadata || typeof metadata !== 'object') { + return undefined + } + + const maybeWithSubBlocks = metadata as typeof metadata & { + subBlocks?: Record + } + + const raw = maybeWithSubBlocks.subBlocks?.[key] + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { + return undefined + } + + return (raw as { value?: unknown }).value +} + +function extractInputFormat(block: SerializedBlock): InputFormatField[] { + const fromMetadata = readMetadataSubBlockValue(block, 'inputFormat') + const fromParams = block.config?.params?.inputFormat + const source = fromMetadata ?? fromParams + + if (!Array.isArray(source)) { + return [] + } + + return source + .filter((field): field is InputFormatField => isPlainObject(field)) + .map((field) => field) +} + +function coerceValue(type: string | null | undefined, value: unknown): unknown { + if (value === undefined || value === null) { + return value + } + + switch (type) { + case 'string': + return typeof value === 'string' ? value : String(value) + case 'number': { + if (typeof value === 'number') return value + const parsed = Number(value) + return Number.isNaN(parsed) ? value : parsed + } + case 'boolean': { + if (typeof value === 'boolean') return value + if (value === 'true' || value === '1' || value === 1) return true + if (value === 'false' || value === '0' || value === 0) return false + return value + } + case 'object': + case 'array': { + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value) + return parsed + } catch { + return value + } + } + return value + } + default: + return value + } +} + +interface DerivedInputResult { + structuredInput: Record + finalInput: unknown + hasStructured: boolean +} + +function deriveInputFromFormat( + inputFormat: InputFormatField[], + workflowInput: unknown, + isDeployedExecution: boolean +): DerivedInputResult { + const structuredInput: Record = {} + + if (inputFormat.length === 0) { + return { + structuredInput, + finalInput: getRawInputCandidate(workflowInput), + hasStructured: false, + } + } + + for (const field of inputFormat) { + const fieldName = field.name?.trim() + if (!fieldName) continue + + let fieldValue: unknown + const workflowRecord = isPlainObject(workflowInput) ? workflowInput : undefined + + if (workflowRecord) { + const inputContainer = workflowRecord.input + if (isPlainObject(inputContainer) && Object.hasOwn(inputContainer, fieldName)) { + fieldValue = inputContainer[fieldName] + } else if (Object.hasOwn(workflowRecord, fieldName)) { + fieldValue = workflowRecord[fieldName] + } + } + + if ((fieldValue === undefined || fieldValue === null) && !isDeployedExecution) { + fieldValue = field.value + } + + structuredInput[fieldName] = coerceValue(field.type, fieldValue) + } + + const hasStructured = Object.keys(structuredInput).length > 0 + const finalInput = hasStructured ? structuredInput : getRawInputCandidate(workflowInput) + + return { + structuredInput, + finalInput, + hasStructured, + } +} + +function getRawInputCandidate(workflowInput: unknown): unknown { + if (isPlainObject(workflowInput) && Object.hasOwn(workflowInput, 'input')) { + return workflowInput.input + } + return workflowInput +} + +function isUserFile(candidate: unknown): candidate is UserFile { + if (!isPlainObject(candidate)) { + return false + } + + return ( + typeof candidate.id === 'string' && + typeof candidate.name === 'string' && + typeof candidate.url === 'string' && + typeof candidate.size === 'number' && + typeof candidate.type === 'string' + ) +} + +function getFilesFromWorkflowInput(workflowInput: unknown): UserFile[] | undefined { + if (!isPlainObject(workflowInput)) { + return undefined + } + const files = workflowInput.files + if (Array.isArray(files) && files.every(isUserFile)) { + return files + } + return undefined +} + +function mergeFilesIntoOutput( + output: NormalizedBlockOutput, + workflowInput: unknown +): NormalizedBlockOutput { + const files = getFilesFromWorkflowInput(workflowInput) + if (files) { + output.files = files + } + return output +} + +function ensureString(value: unknown): string { + return typeof value === 'string' ? value : '' +} + +function buildUnifiedStartOutput(workflowInput: unknown): NormalizedBlockOutput { + const output: NormalizedBlockOutput = {} + + if (isPlainObject(workflowInput)) { + for (const [key, value] of Object.entries(workflowInput)) { + if (key === 'onUploadError') continue + output[key] = value + } + } + + if (!Object.hasOwn(output, 'input')) { + output.input = '' + } + if (!Object.hasOwn(output, 'conversationId')) { + output.conversationId = '' + } + + return mergeFilesIntoOutput(output, workflowInput) +} + +function buildApiOrInputOutput(finalInput: unknown, workflowInput: unknown): NormalizedBlockOutput { + const isObjectInput = isPlainObject(finalInput) + + const output: NormalizedBlockOutput = isObjectInput + ? { + ...(finalInput as Record), + input: { ...(finalInput as Record) }, + } + : { input: finalInput } + + return mergeFilesIntoOutput(output, workflowInput) +} + +function buildChatOutput(workflowInput: unknown): NormalizedBlockOutput { + const source = isPlainObject(workflowInput) ? workflowInput : undefined + + const output: NormalizedBlockOutput = { + input: ensureString(source?.input), + conversationId: ensureString(source?.conversationId), + } + + return mergeFilesIntoOutput(output, workflowInput) +} + +function buildLegacyStarterOutput( + finalInput: unknown, + workflowInput: unknown, + mode: 'manual' | 'api' | 'chat' | null +): NormalizedBlockOutput { + if (mode === 'chat') { + return buildChatOutput(workflowInput) + } + + const output: NormalizedBlockOutput = {} + const finalObject = isPlainObject(finalInput) ? finalInput : undefined + + if (finalObject) { + Object.assign(output, finalObject) + output.input = { ...finalObject } + } else { + output.input = finalInput + } + + const conversationId = isPlainObject(workflowInput) ? workflowInput.conversationId : undefined + if (conversationId !== undefined) { + output.conversationId = ensureString(conversationId) + } + + return mergeFilesIntoOutput(output, workflowInput) +} + +function buildManualTriggerOutput( + finalInput: unknown, + workflowInput: unknown +): NormalizedBlockOutput { + const finalObject = isPlainObject(finalInput) ? finalInput : undefined + + const output: NormalizedBlockOutput = finalObject + ? { ...(finalObject as Record) } + : { input: finalInput } + + if (!Object.hasOwn(output, 'input')) { + output.input = getRawInputCandidate(workflowInput) + } + + return mergeFilesIntoOutput(output, workflowInput) +} + +function extractSubBlocks(block: SerializedBlock): Record | undefined { + const metadata = block.metadata + if (!metadata || typeof metadata !== 'object') { + return undefined + } + + const maybeWithSubBlocks = metadata as typeof metadata & { + subBlocks?: Record + } + + const subBlocks = maybeWithSubBlocks.subBlocks + if (subBlocks && typeof subBlocks === 'object' && !Array.isArray(subBlocks)) { + return subBlocks + } + + return undefined +} + +export interface StartBlockOutputOptions { + resolution: ExecutorStartResolution + workflowInput: unknown + isDeployedExecution: boolean +} + +export function buildStartBlockOutput(options: StartBlockOutputOptions): NormalizedBlockOutput { + const { resolution, workflowInput, isDeployedExecution } = options + const inputFormat = extractInputFormat(resolution.block) + const { finalInput } = deriveInputFromFormat(inputFormat, workflowInput, isDeployedExecution) + + switch (resolution.path) { + case StartBlockPath.UNIFIED: + return buildUnifiedStartOutput(workflowInput) + case StartBlockPath.SPLIT_API: + case StartBlockPath.SPLIT_INPUT: + return buildApiOrInputOutput(finalInput, workflowInput) + case StartBlockPath.SPLIT_CHAT: + return buildChatOutput(workflowInput) + case StartBlockPath.SPLIT_MANUAL: + return buildManualTriggerOutput(finalInput, workflowInput) + case StartBlockPath.LEGACY_STARTER: + return buildLegacyStarterOutput( + finalInput, + workflowInput, + getLegacyStarterMode({ subBlocks: extractSubBlocks(resolution.block) }) + ) + default: + return buildManualTriggerOutput(finalInput, workflowInput) + } +} diff --git a/apps/sim/lib/workflows/block-outputs.ts b/apps/sim/lib/workflows/block-outputs.ts index e53c3e85f7..84dd10ecc2 100644 --- a/apps/sim/lib/workflows/block-outputs.ts +++ b/apps/sim/lib/workflows/block-outputs.ts @@ -1,3 +1,4 @@ +import { classifyStartBlockType, StartBlockPath } from '@/lib/workflows/triggers' import { getBlock } from '@/blocks' import type { BlockConfig } from '@/blocks/types' import { getTrigger } from '@/triggers' @@ -61,7 +62,9 @@ export function getBlockOutputs( let outputs = { ...(blockConfig.outputs || {}) } - if (blockType === 'start_trigger') { + const startPath = classifyStartBlockType(blockType) + + if (startPath === StartBlockPath.UNIFIED) { outputs = { input: { type: 'string', description: 'Primary user input or message' }, conversationId: { type: 'string', description: 'Conversation thread identifier' }, @@ -83,7 +86,7 @@ export function getBlockOutputs( } // Special handling for starter block (legacy) - if (blockType === 'starter') { + if (startPath === StartBlockPath.LEGACY_STARTER) { const startWorkflowValue = subBlocks?.startWorkflow?.value if (startWorkflowValue === 'chat') { diff --git a/apps/sim/lib/workflows/defaults.ts b/apps/sim/lib/workflows/defaults.ts new file mode 100644 index 0000000000..2c992b9c0e --- /dev/null +++ b/apps/sim/lib/workflows/defaults.ts @@ -0,0 +1,125 @@ +import { getBlockOutputs } from '@/lib/workflows/block-outputs' +import { getBlock } from '@/blocks' +import type { BlockConfig, SubBlockConfig } from '@/blocks/types' +import type { BlockState, SubBlockState, WorkflowState } from '@/stores/workflows/workflow/types' + +export interface DefaultWorkflowArtifacts { + workflowState: WorkflowState + subBlockValues: Record> + startBlockId: string +} + +const START_BLOCK_TYPE = 'start_trigger' +const DEFAULT_START_POSITION = { x: 0, y: 0 } + +function cloneDefaultValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map((item) => cloneDefaultValue(item)) + } + + if (value && typeof value === 'object') { + return { ...(value as Record) } + } + + return value ?? null +} + +function resolveInitialValue(subBlock: SubBlockConfig): unknown { + if (typeof subBlock.value === 'function') { + try { + return cloneDefaultValue(subBlock.value({})) + } catch (error) { + // Ignore resolution errors and fall back to default/null values + } + } + + if (subBlock.defaultValue !== undefined) { + return cloneDefaultValue(subBlock.defaultValue) + } + + // Ensure structured fields are initialized with empty collections by default + if (subBlock.type === 'input-format' || subBlock.type === 'table') { + return [] + } + + return null +} + +function buildStartBlockConfig(): BlockConfig { + const blockConfig = getBlock(START_BLOCK_TYPE) + + if (!blockConfig) { + throw new Error('Start trigger block configuration is not registered') + } + + return blockConfig +} + +function buildStartBlockState( + blockConfig: BlockConfig, + blockId: string +): { blockState: BlockState; subBlockValues: Record } { + const subBlocks: Record = {} + const subBlockValues: Record = {} + + blockConfig.subBlocks.forEach((config) => { + const initialValue = resolveInitialValue(config) + + subBlocks[config.id] = { + id: config.id, + type: config.type, + value: (initialValue ?? null) as SubBlockState['value'], + } + + subBlockValues[config.id] = initialValue ?? null + }) + + const outputs = getBlockOutputs(blockConfig.type, subBlocks) + + const blockState: BlockState = { + id: blockId, + type: blockConfig.type, + name: blockConfig.name, + position: { ...DEFAULT_START_POSITION }, + subBlocks, + outputs, + enabled: true, + horizontalHandles: true, + isWide: false, + advancedMode: false, + triggerMode: false, + height: 0, + data: {}, + } + + return { blockState, subBlockValues } +} + +export function buildDefaultWorkflowArtifacts(): DefaultWorkflowArtifacts { + const blockConfig = buildStartBlockConfig() + const startBlockId = crypto.randomUUID() + + const { blockState, subBlockValues } = buildStartBlockState(blockConfig, startBlockId) + + const workflowState: WorkflowState = { + blocks: { + [startBlockId]: blockState, + }, + edges: [], + loops: {}, + parallels: {}, + lastSaved: Date.now(), + isDeployed: false, + deployedAt: undefined, + deploymentStatuses: {}, + needsRedeployment: false, + } + + return { + workflowState, + subBlockValues: { + [startBlockId]: subBlockValues, + }, + startBlockId, + } +} diff --git a/apps/sim/lib/workflows/triggers.ts b/apps/sim/lib/workflows/triggers.ts index 0b1f73b201..c70dd6a161 100644 --- a/apps/sim/lib/workflows/triggers.ts +++ b/apps/sim/lib/workflows/triggers.ts @@ -16,6 +16,185 @@ export const TRIGGER_TYPES = { export type TriggerType = (typeof TRIGGER_TYPES)[keyof typeof TRIGGER_TYPES] +export enum StartBlockPath { + UNIFIED = 'unified_start', + LEGACY_STARTER = 'legacy_starter', + SPLIT_INPUT = 'legacy_input_trigger', + SPLIT_API = 'legacy_api_trigger', + SPLIT_CHAT = 'legacy_chat_trigger', + SPLIT_MANUAL = 'legacy_manual_trigger', +} + +type StartExecutionKind = 'chat' | 'manual' | 'api' + +const EXECUTION_PRIORITIES: Record = { + chat: [StartBlockPath.UNIFIED, StartBlockPath.SPLIT_CHAT, StartBlockPath.LEGACY_STARTER], + manual: [ + StartBlockPath.UNIFIED, + StartBlockPath.SPLIT_API, + StartBlockPath.SPLIT_INPUT, + StartBlockPath.SPLIT_MANUAL, + StartBlockPath.LEGACY_STARTER, + ], + api: [ + StartBlockPath.UNIFIED, + StartBlockPath.SPLIT_API, + StartBlockPath.SPLIT_INPUT, + StartBlockPath.LEGACY_STARTER, + ], +} + +const CHILD_PRIORITIES: StartBlockPath[] = [ + StartBlockPath.UNIFIED, + StartBlockPath.SPLIT_INPUT, + StartBlockPath.LEGACY_STARTER, +] + +const START_CONFLICT_TYPES: TriggerType[] = [ + TRIGGER_TYPES.START, + TRIGGER_TYPES.API, + TRIGGER_TYPES.INPUT, + TRIGGER_TYPES.MANUAL, + TRIGGER_TYPES.CHAT, +] + +type BlockWithType = { type: string; subBlocks?: Record | undefined } + +export interface StartBlockCandidate { + blockId: string + block: T + path: StartBlockPath +} + +export function classifyStartBlockType(type: string): StartBlockPath | null { + switch (type) { + case TRIGGER_TYPES.START: + return StartBlockPath.UNIFIED + case TRIGGER_TYPES.STARTER: + return StartBlockPath.LEGACY_STARTER + case TRIGGER_TYPES.INPUT: + return StartBlockPath.SPLIT_INPUT + case TRIGGER_TYPES.API: + return StartBlockPath.SPLIT_API + case TRIGGER_TYPES.CHAT: + return StartBlockPath.SPLIT_CHAT + case TRIGGER_TYPES.MANUAL: + return StartBlockPath.SPLIT_MANUAL + default: + return null + } +} + +export function classifyStartBlock(block: T): StartBlockPath | null { + return classifyStartBlockType(block.type) +} + +export function isLegacyStartPath(path: StartBlockPath): boolean { + return path !== StartBlockPath.UNIFIED +} + +function toEntries(blocks: Record | T[]): Array<[string, T]> { + if (Array.isArray(blocks)) { + return blocks.map((block, index) => { + const potentialId = (block as { id?: unknown }).id + const inferredId = typeof potentialId === 'string' ? potentialId : `${index}` + return [inferredId, block] + }) + } + return Object.entries(blocks) +} + +type ResolveStartOptions = { + execution: StartExecutionKind + isChildWorkflow?: boolean + allowLegacyStarter?: boolean +} + +function supportsExecution(path: StartBlockPath, execution: StartExecutionKind): boolean { + if (path === StartBlockPath.UNIFIED || path === StartBlockPath.LEGACY_STARTER) { + return true + } + + if (execution === 'chat') { + return path === StartBlockPath.SPLIT_CHAT + } + + if (execution === 'api') { + return path === StartBlockPath.SPLIT_API || path === StartBlockPath.SPLIT_INPUT + } + + return ( + path === StartBlockPath.SPLIT_API || + path === StartBlockPath.SPLIT_INPUT || + path === StartBlockPath.SPLIT_MANUAL + ) +} + +export function resolveStartCandidates( + blocks: Record | T[], + options: ResolveStartOptions +): StartBlockCandidate[] { + const entries = toEntries(blocks) + if (entries.length === 0) return [] + + const priorities = options.isChildWorkflow + ? CHILD_PRIORITIES + : EXECUTION_PRIORITIES[options.execution] + + const candidates: StartBlockCandidate[] = [] + + for (const [blockId, block] of entries) { + const path = classifyStartBlock(block) + if (!path) continue + + if (options.isChildWorkflow) { + if (!CHILD_PRIORITIES.includes(path)) { + continue + } + } else if (!supportsExecution(path, options.execution)) { + continue + } + + if (path === StartBlockPath.LEGACY_STARTER && options.allowLegacyStarter === false) { + continue + } + + candidates.push({ blockId, block, path }) + } + + candidates.sort((a, b) => { + const order = options.isChildWorkflow ? CHILD_PRIORITIES : priorities + const aIdx = order.indexOf(a.path) + const bIdx = order.indexOf(b.path) + if (aIdx === -1 && bIdx === -1) return 0 + if (aIdx === -1) return 1 + if (bIdx === -1) return -1 + return aIdx - bIdx + }) + + return candidates +} + +type SubBlockWithValue = { value?: unknown } + +function readSubBlockValue(subBlocks: Record | undefined, key: string): unknown { + const raw = subBlocks?.[key] + if (raw && typeof raw === 'object' && !Array.isArray(raw)) { + return (raw as SubBlockWithValue).value + } + return undefined +} + +export function getLegacyStarterMode(block: { + subBlocks?: Record +}): 'manual' | 'api' | 'chat' | null { + const modeValue = readSubBlockValue(block.subBlocks, 'startWorkflow') + if (modeValue === 'chat') return 'chat' + if (modeValue === 'api' || modeValue === 'run') return 'api' + if (modeValue === undefined || modeValue === 'manual') return 'manual' + return null +} + /** * Mapping from reference alias (used in inline refs like , , etc.) * to concrete trigger block type identifiers used across the system. @@ -189,25 +368,18 @@ export class TriggerUtils { blocks: Record, executionType: 'chat' | 'manual' | 'api', isChildWorkflow = false - ): { blockId: string; block: T } | null { - const entries = Object.entries(blocks) - - // Look for new trigger blocks first - const triggers = TriggerUtils.findTriggersByType(blocks, executionType, isChildWorkflow) - if (triggers.length > 0) { - const blockId = entries.find(([, b]) => b === triggers[0])?.[0] - if (blockId) { - return { blockId, block: triggers[0] } - } - } + ): (StartBlockCandidate & { block: T }) | null { + const candidates = resolveStartCandidates(blocks, { + execution: executionType, + isChildWorkflow, + }) - // Legacy fallback: look for starter block - const starterEntry = entries.find(([, block]) => block.type === TRIGGER_TYPES.STARTER) - if (starterEntry) { - return { blockId: starterEntry[0], block: starterEntry[1] } + if (candidates.length === 0) { + return null } - return null + const [primary] = candidates + return primary } /** @@ -286,15 +458,7 @@ export class TriggerUtils { // Start trigger cannot coexist with other single-instance trigger types if (triggerType === TRIGGER_TYPES.START) { - return blockArray.some((block) => - [ - TRIGGER_TYPES.START, - TRIGGER_TYPES.API, - TRIGGER_TYPES.INPUT, - TRIGGER_TYPES.MANUAL, - TRIGGER_TYPES.CHAT, - ].includes(block.type as TriggerType) - ) + return blockArray.some((block) => START_CONFLICT_TYPES.includes(block.type as TriggerType)) } // Only one Input trigger allowed diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index 3dac58842c..93ed9abf54 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -2,6 +2,7 @@ import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { createLogger } from '@/lib/logs/console/logger' import { generateCreativeWorkflowName } from '@/lib/naming' +import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { API_ENDPOINTS } from '@/stores/constants' import { useVariablesStore } from '@/stores/panel/variables/store' import type { @@ -564,76 +565,33 @@ export const useWorkflowRegistry = create()( // Initialize subblock values to ensure they're available for sync if (!options.marketplaceId) { - // For non-marketplace workflows, create a default Start block - const starterId = crypto.randomUUID() - const defaultStartBlock = { - id: starterId, - type: 'start_trigger', - name: 'Start', - position: { x: 200, y: 300 }, - enabled: true, - horizontalHandles: true, - isWide: false, - advancedMode: false, - triggerMode: false, - height: 0, - subBlocks: { - inputFormat: { - id: 'inputFormat', - type: 'input-format', - value: [], - }, - }, - outputs: { - input: { type: 'string', description: 'Primary user input or message' }, - conversationId: { type: 'string', description: 'Conversation thread identifier' }, - files: { type: 'files', description: 'User uploaded files' }, - }, - data: {}, - } + const { workflowState, subBlockValues } = buildDefaultWorkflowArtifacts() - // Initialize subblock values useSubBlockStore.setState((state) => ({ workflowValues: { ...state.workflowValues, - [serverWorkflowId]: { - [starterId]: { - inputFormat: [], - }, - }, + [serverWorkflowId]: subBlockValues, }, })) - // Persist the default Start block to database immediately - - ;(async () => { - try { - logger.info(`Persisting default Start block for new workflow ${serverWorkflowId}`) - const response = await fetch(`/api/workflows/${serverWorkflowId}/state`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - blocks: { - [starterId]: defaultStartBlock, - }, - edges: [], - loops: {}, - parallels: {}, - lastSaved: Date.now(), - }), - }) - - if (!response.ok) { - logger.error('Failed to persist default Start block:', await response.text()) - } else { - logger.info('Successfully persisted default Start block') - } - } catch (error) { - logger.error('Error persisting default Start block:', error) + try { + logger.info(`Persisting default Start block for new workflow ${serverWorkflowId}`) + const response = await fetch(`/api/workflows/${serverWorkflowId}/state`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(workflowState), + }) + + if (!response.ok) { + logger.error('Failed to persist default Start block:', await response.text()) + } else { + logger.info('Successfully persisted default Start block') } - })() + } catch (error) { + logger.error('Error persisting default Start block:', error) + } } // Don't set as active workflow here - let the navigation/URL change handle that @@ -831,106 +789,12 @@ export const useWorkflowRegistry = create()( parallels: currentWorkflowState.parallels || {}, } } else { - // Source is not active workflow, create with starter block for now - // In a future enhancement, we could fetch from DB - const starterId = crypto.randomUUID() - const starterBlock = { - id: starterId, - type: 'starter' as const, - name: 'Start', - position: { x: 100, y: 100 }, - subBlocks: { - startWorkflow: { - id: 'startWorkflow', - type: 'dropdown' as const, - value: 'manual', - }, - webhookPath: { - id: 'webhookPath', - type: 'short-input' as const, - value: '', - }, - webhookSecret: { - id: 'webhookSecret', - type: 'short-input' as const, - value: '', - }, - scheduleType: { - id: 'scheduleType', - type: 'dropdown' as const, - value: 'daily', - }, - minutesInterval: { - id: 'minutesInterval', - type: 'short-input' as const, - value: '', - }, - minutesStartingAt: { - id: 'minutesStartingAt', - type: 'short-input' as const, - value: '', - }, - hourlyMinute: { - id: 'hourlyMinute', - type: 'short-input' as const, - value: '', - }, - dailyTime: { - id: 'dailyTime', - type: 'short-input' as const, - value: '', - }, - weeklyDay: { - id: 'weeklyDay', - type: 'dropdown' as const, - value: 'MON', - }, - weeklyDayTime: { - id: 'weeklyDayTime', - type: 'short-input' as const, - value: '', - }, - monthlyDay: { - id: 'monthlyDay', - type: 'short-input' as const, - value: '', - }, - monthlyTime: { - id: 'monthlyTime', - type: 'short-input' as const, - value: '', - }, - cronExpression: { - id: 'cronExpression', - type: 'short-input' as const, - value: '', - }, - timezone: { - id: 'timezone', - type: 'dropdown' as const, - value: 'UTC', - }, - }, - outputs: { - response: { - type: { - input: 'any', - }, - }, - }, - enabled: true, - horizontalHandles: true, - isWide: false, - advancedMode: false, - triggerMode: false, - height: 0, - } - + const { workflowState } = buildDefaultWorkflowArtifacts() sourceState = { - blocks: { [starterId]: starterBlock }, - edges: [], - loops: {}, - parallels: {}, + blocks: workflowState.blocks, + edges: workflowState.edges, + loops: workflowState.loops, + parallels: workflowState.parallels, } } From 815b0bccd50808b2304357e65129369cde18e3f1 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 28 Oct 2025 16:47:53 -0700 Subject: [PATCH 6/8] debounce status checks --- .../components/control-bar/control-bar.tsx | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx index 33cd1c003e..a8741720bd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx @@ -266,14 +266,36 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) { activeWorkflowId ? state.workflowValues[activeWorkflowId] : null ) + const [blockStructureVersion, setBlockStructureVersion] = useState(0) + const [edgeStructureVersion, setEdgeStructureVersion] = useState(0) + const [subBlockStructureVersion, setSubBlockStructureVersion] = useState(0) + + useEffect(() => { + setBlockStructureVersion((version) => version + 1) + }, [currentBlocks]) + + useEffect(() => { + setEdgeStructureVersion((version) => version + 1) + }, [currentEdges]) + + useEffect(() => { + setSubBlockStructureVersion((version) => version + 1) + }, [subBlockValues]) + + useEffect(() => { + setBlockStructureVersion(0) + setEdgeStructureVersion(0) + setSubBlockStructureVersion(0) + }, [activeWorkflowId]) + const statusCheckTrigger = useMemo(() => { return JSON.stringify({ - blocks: Object.keys(currentBlocks || {}).length, - edges: currentEdges?.length || 0, - subBlocks: Object.keys(subBlockValues || {}).length, - timestamp: Date.now(), + lastSaved: lastSaved ?? 0, + blockVersion: blockStructureVersion, + edgeVersion: edgeStructureVersion, + subBlockVersion: subBlockStructureVersion, }) - }, [currentBlocks, currentEdges, subBlockValues]) + }, [lastSaved, blockStructureVersion, edgeStructureVersion, subBlockStructureVersion]) const debouncedStatusCheckTrigger = useDebounce(statusCheckTrigger, 500) From 66ed5ece4195c61a380eb5ea37672f4aa2e126cd Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 28 Oct 2025 17:12:37 -0700 Subject: [PATCH 7/8] update docs --- apps/docs/content/docs/en/triggers/start.mdx | 90 ++++++++++++++++++ .../docs/content/docs/en/triggers/starter.mdx | 67 ------------- apps/docs/public/static/start.png | Bin 0 -> 19724 bytes apps/docs/public/static/starter.png | Bin 37728 -> 0 bytes 4 files changed, 90 insertions(+), 67 deletions(-) create mode 100644 apps/docs/content/docs/en/triggers/start.mdx delete mode 100644 apps/docs/content/docs/en/triggers/starter.mdx create mode 100644 apps/docs/public/static/start.png delete mode 100644 apps/docs/public/static/starter.png diff --git a/apps/docs/content/docs/en/triggers/start.mdx b/apps/docs/content/docs/en/triggers/start.mdx new file mode 100644 index 0000000000..53d1d3aed3 --- /dev/null +++ b/apps/docs/content/docs/en/triggers/start.mdx @@ -0,0 +1,90 @@ +--- +title: Start +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' +import { Image } from '@/components/ui/image' + +The Start block is the default trigger for workflows built in Sim. It collects structured inputs and fans out to the rest of your graph for editor tests, API deployments, and chat experiences. + +
+ Start block with Input Format fields +
+ + +The Start block sits in the start slot when you create a workflow. Keep it there when you want the same entry point to serve editor runs, deploy-to-API requests, and chat sessions. Swap it with Webhook or Schedule triggers when you only need event-driven execution. + + +## Fields exposed by Start + +The Start block emits different data depending on the execution surface: + +- **Input Format fields** — Every field you add becomes available as ``. For example, a `customerId` field shows up as `` in downstream blocks and templates. +- **Chat-only fields** — When the workflow runs from the chat side panel or a deployed chat experience, Sim also provides `` (latest user message), `` (active session id), and `` (chat attachments). + +Keep Input Format fields scoped to the names you expect to reference later—those values are the only structured fields shared across editor, API, and chat runs. + +## Configure the Input Format + +Use the Input Format sub-block to define the schema that applies across execution modes: + +1. Add a field for each value you want to collect. +2. Choose a type (`string`, `number`, `boolean`, `object`, `array`, or `files`). File fields accept uploads from chat and API callers. +3. Provide default values when you want the manual run modal to populate test data automatically. These defaults are ignored for deployed executions. +4. Reorder fields to control how they appear in the editor form. + +Reference structured values downstream with expressions such as `` depending on the block you connect. + +## How it behaves per entry point + + + +
+

+ When you click Run in the editor, the Start block renders the Input Format as a form. Default values make it easy to retest without retyping data. Submitting the form triggers the workflow immediately and the values become available on <start.fieldName> (for example <start.sampleField>). +

+

+ File fields in the form upload directly into the corresponding ``; use those values to feed downstream tools or storage steps. +

+
+
+ +
+

+ Deploying to API turns the Input Format into a JSON contract for clients. Each field becomes part of the request body, and Sim coerces primitive types on ingestion. File fields expect objects that reference uploaded files; use the execution file upload endpoint before invoking the workflow. +

+

+ API callers can include additional optional properties. They are preserved inside `` outputs so you can experiment without redeploying immediately. +

+
+
+ +
+

+ In chat deployments the Start block binds to the active conversation. The latest message fills <start.input>, the session identifier is available at <start.conversationId>, and user attachments appear on <start.files>, alongside any Input Format fields scoped as <start.fieldName>. +

+

+ If you launch chat with additional structured context (for example from an embed), it merges into the corresponding `` outputs, keeping downstream blocks consistent with API and manual runs. +

+
+
+
+ +## Referencing Start data downstream + +- Connect `` directly into agents, tools, or functions that expect structured payloads. +- Use templating syntax like `` or `` (chat only) in prompt fields. +- Keep `` handy when you need to group outputs, update conversation history, or call back into the chat API. + +## Best practices + +- Treat the Start block as the single entry point when you want to support both API and chat callers. +- Prefer named Input Format fields over parsing raw JSON in downstream nodes; type coercion happens automatically. +- Add validation or routing immediately after Start if certain fields are required for your workflow to succeed. \ No newline at end of file diff --git a/apps/docs/content/docs/en/triggers/starter.mdx b/apps/docs/content/docs/en/triggers/starter.mdx deleted file mode 100644 index 9d2322cb5d..0000000000 --- a/apps/docs/content/docs/en/triggers/starter.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Starter (Deprecated) ---- - -import { Callout } from 'fumadocs-ui/components/callout' -import { Tab, Tabs } from 'fumadocs-ui/components/tabs' -import { Image } from '@/components/ui/image' -import { Video } from '@/components/ui/video' - - -The Starter block has been deprecated and replaced with more specialized Core Triggers. Please see the [Core Triggers documentation](/triggers) for the new API, Chat, Input Form, Manual, Schedule, and Webhook triggers. - - -The Starter block allows you to manually initiate workflow execution with input parameters, offering two input modes: structured parameters or conversational chat. - -
- Starter Block with Manual and Chat Mode Options -
- -## Execution Modes - -Choose your input method from the dropdown: - - - -
-
    -
  • API Friendly Structured inputs: Define specific parameters (text, number, boolean, JSON, file, date)
  • -
  • Testing while Building Your Workflow: Quick iteration while debugging workflows
  • -
- -
-
- -

Configure input parameters that will be available when deploying as an API endpoint.

-
-
- -
-
    -
  • Natural language: Users type questions or requests
  • -
  • Conversational: Ideal for AI-powered workflows
  • -
- -
-
- -

Chat with your workflow and access input text, conversation ID, and uploaded files for context-aware responses.

-
-
-
- -## Using Chat Variables - -In Chat mode, access user input and conversation context through special variables: - -- **``** - Contains the user's message text -- **``** - Unique identifier for the conversation thread -- **``** - Array of files uploaded by the user (if any) \ No newline at end of file diff --git a/apps/docs/public/static/start.png b/apps/docs/public/static/start.png new file mode 100644 index 0000000000000000000000000000000000000000..45368bbd9188bbaa6cdd2db4d3a42ec838121b61 GIT binary patch literal 19724 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 a+JgG*7T;eLEvYMhY4Gz3^t|sLp7tNFx(FEn literal 0 HcmV?d00001 diff --git a/apps/docs/public/static/starter.png b/apps/docs/public/static/starter.png deleted file mode 100644 index 96a1c086e5238ddb64ea85e103d765cb1313e2bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37728 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 From c4727e05bbffeca1549cca8db8c9ca6e6534d8a9 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 31 Oct 2025 12:22:15 -0700 Subject: [PATCH 8/8] remove unused code --- apps/sim/stores/workflows/registry/store.ts | 48 ++++++++++----------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index 41139a3358..32ffb767fb 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -547,34 +547,32 @@ export const useWorkflowRegistry = create()( })) // Initialize subblock values to ensure they're available for sync - if (!options.marketplaceId) { - const { workflowState, subBlockValues } = buildDefaultWorkflowArtifacts() + const { workflowState, subBlockValues } = buildDefaultWorkflowArtifacts() - useSubBlockStore.setState((state) => ({ - workflowValues: { - ...state.workflowValues, - [serverWorkflowId]: subBlockValues, + useSubBlockStore.setState((state) => ({ + workflowValues: { + ...state.workflowValues, + [serverWorkflowId]: subBlockValues, + }, + })) + + try { + logger.info(`Persisting default Start block for new workflow ${serverWorkflowId}`) + const response = await fetch(`/api/workflows/${serverWorkflowId}/state`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', }, - })) - - try { - logger.info(`Persisting default Start block for new workflow ${serverWorkflowId}`) - const response = await fetch(`/api/workflows/${serverWorkflowId}/state`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(workflowState), - }) - - if (!response.ok) { - logger.error('Failed to persist default Start block:', await response.text()) - } else { - logger.info('Successfully persisted default Start block') - } - } catch (error) { - logger.error('Error persisting default Start block:', error) + body: JSON.stringify(workflowState), + }) + + if (!response.ok) { + logger.error('Failed to persist default Start block:', await response.text()) + } else { + logger.info('Successfully persisted default Start block') } + } catch (error) { + logger.error('Error persisting default Start block:', error) } // Don't set as active workflow here - let the navigation/URL change handle that