From 5ee66252edfb5c50870a6051820d2b5aee3d0e1f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 19 Jul 2025 21:22:51 -0700 Subject: [PATCH] feat(webhook-triggers): multiple webhook trigger blocks (#725) * checkpoint * correctly clear status * works * improvements * fix build issue * add docs * remove comments, logs * fix migration to have foreign ref key * change filename to snake case * modified dropdown to match combobox styling * added block type for triggers, split out schedule block into a separate trigger * added chat trigger to start block, removed startAt from schedule modal, added chat fields into tag dropdown for start block * removed startedAt for schedules, separated schedules into a separate block, removed unique constraint on scheule workflows and added combo constraint on workflowid/blockid and schedule * icons fix --------- Co-authored-by: Waleed Latif --- apps/docs/content/docs/blocks/meta.json | 11 +- .../content/docs/blocks/webhook_trigger.mdx | 113 + .../static/dark/webhooktrigger-dark.png | Bin 0 -> 35233 bytes .../static/light/webhooktrigger-light.png | Bin 0 -> 36608 bytes apps/sim/app/api/schedules/[id]/route.ts | 23 + apps/sim/app/api/schedules/execute/route.ts | 16 +- apps/sim/app/api/schedules/route.ts | 97 +- apps/sim/app/api/webhooks/route.ts | 25 +- .../[id]/revert-to-deployed/route.ts | 1 - apps/sim/app/api/workflows/[id]/route.ts | 1 - .../sim/app/api/workflows/[id]/state/route.ts | 2 - .../sub-block/components/dropdown.tsx | 315 +- .../schedule/components/schedule-modal.tsx | 114 +- .../components/schedule/schedule-config.tsx | 117 +- .../sub-block/components/webhook/webhook.tsx | 189 +- .../workflow-block/workflow-block.tsx | 157 +- .../workflow-text-editor/workflow-applier.ts | 1 - .../hooks/use-workflow-execution.ts | 38 +- .../[workspaceId]/w/[workflowId]/utils.ts | 16 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 10 + .../components/search-modal/search-modal.tsx | 7 +- .../sidebar/components/toolbar/toolbar.tsx | 25 +- apps/sim/blocks/blocks/schedule.ts | 116 + apps/sim/blocks/blocks/starter.ts | 148 +- apps/sim/blocks/blocks/webhook.ts | 92 + apps/sim/blocks/registry.ts | 6 +- apps/sim/blocks/types.ts | 2 +- apps/sim/components/icons.tsx | 38 + apps/sim/components/ui/tag-dropdown.tsx | 68 +- apps/sim/contexts/socket-context.tsx | 2 +- .../migrations/0057_charming_star_brand.sql | 6 + .../sim/db/migrations/meta/0057_snapshot.json | 5655 +++++++++++++++++ apps/sim/db/migrations/meta/_journal.json | 7 + apps/sim/db/schema.ts | 46 +- apps/sim/executor/consts.ts | 2 + apps/sim/executor/index.ts | 143 +- apps/sim/hooks/use-collaborative-workflow.ts | 1 - apps/sim/lib/webhooks/utils.ts | 15 +- apps/sim/lib/workflows/db-helpers.test.ts | 4 - apps/sim/lib/workflows/db-helpers.ts | 2 - apps/sim/socket-server/database/operations.ts | 1 - apps/sim/stores/workflows/registry/store.ts | 4 - apps/sim/stores/workflows/workflow/store.ts | 16 - apps/sim/stores/workflows/workflow/types.ts | 2 - apps/sim/stores/workflows/yaml/importer.ts | 1 - 45 files changed, 7028 insertions(+), 627 deletions(-) create mode 100644 apps/docs/content/docs/blocks/webhook_trigger.mdx create mode 100644 apps/docs/public/static/dark/webhooktrigger-dark.png create mode 100644 apps/docs/public/static/light/webhooktrigger-light.png create mode 100644 apps/sim/blocks/blocks/schedule.ts create mode 100644 apps/sim/blocks/blocks/webhook.ts create mode 100644 apps/sim/db/migrations/0057_charming_star_brand.sql create mode 100644 apps/sim/db/migrations/meta/0057_snapshot.json diff --git a/apps/docs/content/docs/blocks/meta.json b/apps/docs/content/docs/blocks/meta.json index 374ed282c1..d2e8f50c01 100644 --- a/apps/docs/content/docs/blocks/meta.json +++ b/apps/docs/content/docs/blocks/meta.json @@ -4,12 +4,13 @@ "agent", "api", "condition", - "function", "evaluator", - "router", - "response", - "workflow", + "function", "loop", - "parallel" + "parallel", + "response", + "router", + "webhook_trigger", + "workflow" ] } diff --git a/apps/docs/content/docs/blocks/webhook_trigger.mdx b/apps/docs/content/docs/blocks/webhook_trigger.mdx new file mode 100644 index 0000000000..08f9922e51 --- /dev/null +++ b/apps/docs/content/docs/blocks/webhook_trigger.mdx @@ -0,0 +1,113 @@ +--- +title: Webhook Trigger +description: Trigger workflow execution from external webhooks +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Step, Steps } from 'fumadocs-ui/components/steps' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' +import { Card, Cards } from 'fumadocs-ui/components/card' +import { ThemeImage } from '@/components/ui/theme-image' + +The Webhook Trigger block allows external services to trigger your workflow execution through HTTP webhooks. Unlike starter blocks, webhook triggers are pure input sources that start workflows without requiring manual intervention. + + + + + Webhook triggers cannot receive incoming connections and do not expose webhook data to the workflow. They serve as pure execution triggers. + + +## Overview + +The Webhook Trigger block enables you to: + + + + Receive external triggers: Accept HTTP requests from external services + + + Support multiple providers: Handle webhooks from Slack, Gmail, GitHub, and more + + + Start workflows automatically: Execute workflows without manual intervention + + + Provide secure endpoints: Generate unique webhook URLs for each trigger + + + +## How It Works + +The Webhook Trigger block operates as a pure input source: + +1. **Generate Endpoint** - Creates a unique webhook URL when configured +2. **Receive Request** - Accepts HTTP POST requests from external services +3. **Trigger Execution** - Starts the workflow when a valid request is received + +## Configuration Options + +### Webhook Provider + +Choose from supported service providers: + + + + Receive events from Slack apps and bots + + + Handle email-based triggers and notifications + + + Respond to database changes + + + Process bot messages and updates + + + Handle messaging events + + + Process repository events and pull requests + + + Respond to Discord server events + + + Handle payment and subscription events + + + +### Generic Webhooks + +For custom integrations or services not listed above, use the **Generic** provider. This option accepts HTTP POST requests from any client and provides flexible authentication options: + +- **Optional Authentication** - Configure Bearer token or custom header authentication +- **IP Restrictions** - Limit access to specific IP addresses +- **Request Deduplication** - Automatic duplicate request detection using content hashing +- **Flexible Headers** - Support for custom authentication header names + +The Generic provider is ideal for internal services, custom applications, or third-party tools that need to trigger workflows via standard HTTP requests. + +### Webhook Configuration + +Configure provider-specific settings: + +- **Webhook URL** - Automatically generated unique endpoint +- **Provider Settings** - Authentication and validation options +- **Security** - Built-in rate limiting and provider-specific authentication + +## Best Practices + +- **Use unique webhook URLs** for each integration to maintain security +- **Configure proper authentication** when supported by the provider +- **Keep workflows independent** of webhook payload structure +- **Test webhook endpoints** before deploying to production +- **Monitor webhook delivery** through provider dashboards + + diff --git a/apps/docs/public/static/dark/webhooktrigger-dark.png b/apps/docs/public/static/dark/webhooktrigger-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a8cceb5f3cac91a0330d62243f29f03c5d901378 GIT binary patch literal 35233 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 literal 0 HcmV?d00001 diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts index a8f1ff83ef..04a39a8663 100644 --- a/apps/sim/app/api/schedules/[id]/route.ts +++ b/apps/sim/app/api/schedules/[id]/route.ts @@ -141,6 +141,29 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ }) } + if (action === 'disable' || (body.status && body.status === 'disabled')) { + if (schedule.status === 'disabled') { + return NextResponse.json({ message: 'Schedule is already disabled' }, { status: 200 }) + } + + const now = new Date() + + await db + .update(workflowSchedule) + .set({ + status: 'disabled', + updatedAt: now, + nextRunAt: null, // Clear next run time when disabled + }) + .where(eq(workflowSchedule.id, scheduleId)) + + logger.info(`[${requestId}] Disabled schedule: ${scheduleId}`) + + return NextResponse.json({ + message: 'Schedule disabled successfully', + }) + } + logger.warn(`[${requestId}] Unsupported update action for schedule: ${scheduleId}`) return NextResponse.json({ error: 'Unsupported update action' }, { status: 400 }) } catch (error) { diff --git a/apps/sim/app/api/schedules/execute/route.ts b/apps/sim/app/api/schedules/execute/route.ts index 5729b225e4..65845a5e2c 100644 --- a/apps/sim/app/api/schedules/execute/route.ts +++ b/apps/sim/app/api/schedules/execute/route.ts @@ -46,10 +46,13 @@ function calculateNextRunTime( schedule: typeof workflowSchedule.$inferSelect, blocks: Record ): Date { - const starterBlock = Object.values(blocks).find((block) => block.type === 'starter') - if (!starterBlock) throw new Error('No starter block found') - const scheduleType = getSubBlockValue(starterBlock, 'scheduleType') - const scheduleValues = getScheduleTimeValues(starterBlock) + // Look for either starter block or schedule trigger block + const scheduleBlock = Object.values(blocks).find( + (block) => block.type === 'starter' || block.type === 'schedule' + ) + if (!scheduleBlock) throw new Error('No starter or schedule block found') + const scheduleType = getSubBlockValue(scheduleBlock, 'scheduleType') + const scheduleValues = getScheduleTimeValues(scheduleBlock) if (schedule.cronExpression) { const cron = new Cron(schedule.cronExpression) @@ -401,7 +404,10 @@ export async function GET() { // Set up enhanced logging on the executor loggingSession.setupExecutor(executor) - const result = await executor.execute(schedule.workflowId) + const result = await executor.execute( + schedule.workflowId, + schedule.blockId || undefined + ) const executionResult = 'stream' in result && 'execution' in result ? result.execution : result diff --git a/apps/sim/app/api/schedules/route.ts b/apps/sim/app/api/schedules/route.ts index 6c4e4d0128..c6746a0ff0 100644 --- a/apps/sim/app/api/schedules/route.ts +++ b/apps/sim/app/api/schedules/route.ts @@ -1,5 +1,5 @@ import crypto from 'crypto' -import { eq } from 'drizzle-orm' +import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' @@ -19,6 +19,7 @@ const logger = createLogger('ScheduledAPI') const ScheduleRequestSchema = z.object({ workflowId: z.string(), + blockId: z.string().optional(), state: z.object({ blocks: z.record(z.any()), edges: z.array(z.any()), @@ -66,6 +67,7 @@ export async function GET(req: NextRequest) { const requestId = crypto.randomUUID().slice(0, 8) const url = new URL(req.url) const workflowId = url.searchParams.get('workflowId') + const blockId = url.searchParams.get('blockId') const mode = url.searchParams.get('mode') if (mode && mode !== 'schedule') { @@ -92,10 +94,16 @@ export async function GET(req: NextRequest) { recentRequests.set(workflowId, now) } + // Build query conditions + const conditions = [eq(workflowSchedule.workflowId, workflowId)] + if (blockId) { + conditions.push(eq(workflowSchedule.blockId, blockId)) + } + const schedule = await db .select() .from(workflowSchedule) - .where(eq(workflowSchedule.workflowId, workflowId)) + .where(conditions.length > 1 ? and(...conditions) : conditions[0]) .limit(1) const headers = new Headers() @@ -138,36 +146,81 @@ export async function POST(req: NextRequest) { } const body = await req.json() - const { workflowId, state } = ScheduleRequestSchema.parse(body) + const { workflowId, blockId, state } = ScheduleRequestSchema.parse(body) logger.info(`[${requestId}] Processing schedule update for workflow ${workflowId}`) - const starterBlock = Object.values(state.blocks).find( - (block: any) => block.type === 'starter' - ) as BlockState | undefined - - if (!starterBlock) { - logger.warn(`[${requestId}] No starter block found in workflow ${workflowId}`) - return NextResponse.json({ error: 'No starter block found in workflow' }, { status: 400 }) + // Find the target block - prioritize the specific blockId if provided + let targetBlock: BlockState | undefined + if (blockId) { + // If blockId is provided, find that specific block + targetBlock = Object.values(state.blocks).find((block: any) => block.id === blockId) as + | BlockState + | undefined + } else { + // Fallback: find either starter block or schedule trigger block + targetBlock = Object.values(state.blocks).find( + (block: any) => block.type === 'starter' || block.type === 'schedule' + ) as BlockState | undefined } - const startWorkflow = getSubBlockValue(starterBlock, 'startWorkflow') - const scheduleType = getSubBlockValue(starterBlock, 'scheduleType') + if (!targetBlock) { + logger.warn(`[${requestId}] No starter or schedule block found in workflow ${workflowId}`) + return NextResponse.json( + { error: 'No starter or schedule block found in workflow' }, + { status: 400 } + ) + } - const scheduleValues = getScheduleTimeValues(starterBlock) + const startWorkflow = getSubBlockValue(targetBlock, 'startWorkflow') + const scheduleType = getSubBlockValue(targetBlock, 'scheduleType') - const hasScheduleConfig = hasValidScheduleConfig(scheduleType, scheduleValues, starterBlock) + const scheduleValues = getScheduleTimeValues(targetBlock) - if (startWorkflow !== 'schedule' && !hasScheduleConfig) { + const hasScheduleConfig = hasValidScheduleConfig(scheduleType, scheduleValues, targetBlock) + + // For schedule trigger blocks, we always have valid configuration + // For starter blocks, check if schedule is selected and has valid config + const isScheduleBlock = targetBlock.type === 'schedule' + const hasValidConfig = isScheduleBlock || (startWorkflow === 'schedule' && hasScheduleConfig) + + // Debug logging to understand why validation fails + logger.info(`[${requestId}] Schedule validation debug:`, { + workflowId, + blockId, + blockType: targetBlock.type, + isScheduleBlock, + startWorkflow, + scheduleType, + hasScheduleConfig, + hasValidConfig, + scheduleValues: { + minutesInterval: scheduleValues.minutesInterval, + dailyTime: scheduleValues.dailyTime, + cronExpression: scheduleValues.cronExpression, + }, + }) + + if (!hasValidConfig) { logger.info( `[${requestId}] Removing schedule for workflow ${workflowId} - no valid configuration found` ) - await db.delete(workflowSchedule).where(eq(workflowSchedule.workflowId, workflowId)) + // Build delete conditions + const deleteConditions = [eq(workflowSchedule.workflowId, workflowId)] + if (blockId) { + deleteConditions.push(eq(workflowSchedule.blockId, blockId)) + } + + await db + .delete(workflowSchedule) + .where(deleteConditions.length > 1 ? and(...deleteConditions) : deleteConditions[0]) return NextResponse.json({ message: 'Schedule removed' }) } - if (startWorkflow !== 'schedule') { + if (isScheduleBlock) { + logger.info(`[${requestId}] Processing schedule trigger block for workflow ${workflowId}`) + } else if (startWorkflow !== 'schedule') { logger.info( `[${requestId}] Setting workflow to scheduled mode based on schedule configuration` ) @@ -177,12 +230,12 @@ export async function POST(req: NextRequest) { let cronExpression: string | null = null let nextRunAt: Date | undefined - const timezone = getSubBlockValue(starterBlock, 'timezone') || 'UTC' + const timezone = getSubBlockValue(targetBlock, 'timezone') || 'UTC' try { const defaultScheduleType = scheduleType || 'daily' - const scheduleStartAt = getSubBlockValue(starterBlock, 'scheduleStartAt') - const scheduleTime = getSubBlockValue(starterBlock, 'scheduleTime') + const scheduleStartAt = getSubBlockValue(targetBlock, 'scheduleStartAt') + const scheduleTime = getSubBlockValue(targetBlock, 'scheduleTime') logger.debug(`[${requestId}] Schedule configuration:`, { type: defaultScheduleType, @@ -218,6 +271,7 @@ export async function POST(req: NextRequest) { const values = { id: crypto.randomUUID(), workflowId, + blockId, cronExpression, triggerType: 'schedule', createdAt: new Date(), @@ -229,6 +283,7 @@ export async function POST(req: NextRequest) { } const setValues = { + blockId, cronExpression, updatedAt: new Date(), nextRunAt, @@ -241,7 +296,7 @@ export async function POST(req: NextRequest) { .insert(workflowSchedule) .values(values) .onConflictDoUpdate({ - target: [workflowSchedule.workflowId], + target: [workflowSchedule.workflowId, workflowSchedule.blockId], set: setValues, }) diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index 3f86741d52..fabf9ed3fe 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -26,15 +26,30 @@ export async function GET(request: NextRequest) { // Get query parameters const { searchParams } = new URL(request.url) const workflowId = searchParams.get('workflowId') + const blockId = searchParams.get('blockId') + + if (workflowId && !blockId) { + // For now, allow the call but return empty results to avoid breaking the UI + return NextResponse.json({ webhooks: [] }, { status: 200 }) + } logger.debug(`[${requestId}] Fetching webhooks for user ${session.user.id}`, { filteredByWorkflow: !!workflowId, + filteredByBlock: !!blockId, }) // Create where condition - const whereCondition = workflowId - ? and(eq(workflow.userId, session.user.id), eq(webhook.workflowId, workflowId)) - : eq(workflow.userId, session.user.id) + const conditions = [eq(workflow.userId, session.user.id)] + + if (workflowId) { + conditions.push(eq(webhook.workflowId, workflowId)) + } + + if (blockId) { + conditions.push(eq(webhook.blockId, blockId)) + } + + const whereCondition = conditions.length > 1 ? and(...conditions) : conditions[0] const webhooks = await db .select({ @@ -68,7 +83,7 @@ export async function POST(request: NextRequest) { try { const body = await request.json() - const { workflowId, path, provider, providerConfig } = body + const { workflowId, path, provider, providerConfig, blockId } = body // Validate input if (!workflowId || !path) { @@ -115,6 +130,7 @@ export async function POST(request: NextRequest) { const updatedResult = await db .update(webhook) .set({ + blockId, provider, providerConfig, isActive: true, @@ -132,6 +148,7 @@ export async function POST(request: NextRequest) { .values({ id: webhookId, workflowId, + blockId, path, provider, providerConfig, diff --git a/apps/sim/app/api/workflows/[id]/revert-to-deployed/route.ts b/apps/sim/app/api/workflows/[id]/revert-to-deployed/route.ts index 4803912dc9..c66e9930a3 100644 --- a/apps/sim/app/api/workflows/[id]/revert-to-deployed/route.ts +++ b/apps/sim/app/api/workflows/[id]/revert-to-deployed/route.ts @@ -64,7 +64,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ isDeployed: workflowData.isDeployed, deployedAt: workflowData.deployedAt, deploymentStatuses: deployedState.deploymentStatuses || {}, - hasActiveSchedule: deployedState.hasActiveSchedule || false, hasActiveWebhook: deployedState.hasActiveWebhook || false, }) diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index 08d87b6356..d7534f41f3 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -119,7 +119,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ finalWorkflowData.state = { // Default values for expected properties deploymentStatuses: {}, - hasActiveSchedule: false, hasActiveWebhook: false, // Preserve any existing state properties ...existingState, diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts index b66959219b..8a62c00b61 100644 --- a/apps/sim/app/api/workflows/[id]/state/route.ts +++ b/apps/sim/app/api/workflows/[id]/state/route.ts @@ -103,7 +103,6 @@ const WorkflowStateSchema = z.object({ isDeployed: z.boolean().optional(), deployedAt: z.date().optional(), deploymentStatuses: z.record(DeploymentStatusSchema).optional(), - hasActiveSchedule: z.boolean().optional(), hasActiveWebhook: z.boolean().optional(), }) @@ -180,7 +179,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ isDeployed: state.isDeployed || false, deployedAt: state.deployedAt, deploymentStatuses: state.deploymentStatuses || {}, - hasActiveSchedule: state.hasActiveSchedule || false, hasActiveWebhook: state.hasActiveWebhook || false, } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/dropdown.tsx index 8be4f04c64..b23445907a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/dropdown.tsx @@ -1,18 +1,19 @@ -import { useEffect, useMemo, useState } from 'react' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' +import { useEffect, useMemo, useRef, useState } from 'react' +import { Check, ChevronDown } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { cn } from '@/lib/utils' import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler' import { useSubBlockValue } from '../hooks/use-sub-block-value' interface DropdownProps { options: - | Array - | (() => Array) + | Array< + string | { label: string; id: string; icon?: React.ComponentType<{ className?: string }> } + > + | (() => Array< + string | { label: string; id: string; icon?: React.ComponentType<{ className?: string }> } + >) defaultValue?: string blockId: string subBlockId: string @@ -20,6 +21,7 @@ interface DropdownProps { isPreview?: boolean previewValue?: string | null disabled?: boolean + placeholder?: string } export function Dropdown({ @@ -31,9 +33,15 @@ export function Dropdown({ isPreview = false, previewValue, disabled, + placeholder = 'Select an option...', }: DropdownProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [storeInitialized, setStoreInitialized] = useState(false) + const [open, setOpen] = useState(false) + const [highlightedIndex, setHighlightedIndex] = useState(-1) + + const inputRef = useRef(null) + const dropdownRef = useRef(null) // For response dataMode conversion - get builderData and data sub-blocks const [builderData] = useSubBlockValue(blockId, 'builderData') @@ -47,11 +55,19 @@ export function Dropdown({ return typeof options === 'function' ? options() : options }, [options]) - const getOptionValue = (option: string | { label: string; id: string }) => { + const getOptionValue = ( + option: + | string + | { label: string; id: string; icon?: React.ComponentType<{ className?: string }> } + ) => { return typeof option === 'string' ? option : option.id } - const getOptionLabel = (option: string | { label: string; id: string }) => { + const getOptionLabel = ( + option: + | string + | { label: string; id: string; icon?: React.ComponentType<{ className?: string }> } + ) => { return typeof option === 'string' ? option : option.label } @@ -85,67 +101,234 @@ export function Dropdown({ } }, [storeInitialized, value, defaultOptionValue, setStoreValue]) - // Calculate the effective value to use in the dropdown - const effectiveValue = useMemo(() => { - // If we have a value from the store, use that - if (value !== null && value !== undefined) { - return value + // Event handlers + const handleSelect = (selectedValue: string) => { + if (!isPreview && !disabled) { + // Handle conversion when switching from Builder to Editor mode in response blocks + if ( + subBlockId === 'dataMode' && + storeValue === 'structured' && + selectedValue === 'json' && + builderData && + Array.isArray(builderData) && + builderData.length > 0 + ) { + // Convert builderData to JSON string for editor mode + const jsonString = ResponseBlockHandler.convertBuilderDataToJsonString(builderData) + setData(jsonString) + } + + setStoreValue(selectedValue) + } + setOpen(false) + setHighlightedIndex(-1) + inputRef.current?.blur() + } + + const handleDropdownClick = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + if (!disabled) { + setOpen(!open) + if (!open) { + inputRef.current?.focus() + } + } + } + + const handleFocus = () => { + setOpen(true) + setHighlightedIndex(-1) + } + + const handleBlur = () => { + // Delay closing to allow dropdown selection + setTimeout(() => { + const activeElement = document.activeElement + if (!activeElement || !activeElement.closest('.absolute.top-full')) { + setOpen(false) + setHighlightedIndex(-1) + } + }, 150) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setOpen(false) + setHighlightedIndex(-1) + return } - // Only return defaultOptionValue if store is initialized - if (storeInitialized) { - return defaultOptionValue + if (e.key === 'ArrowDown') { + e.preventDefault() + if (!open) { + setOpen(true) + setHighlightedIndex(0) + } else { + setHighlightedIndex((prev) => (prev < evaluatedOptions.length - 1 ? prev + 1 : 0)) + } } - // While store is loading, don't use any value - return undefined - }, [value, defaultOptionValue, storeInitialized]) + if (e.key === 'ArrowUp') { + e.preventDefault() + if (open) { + setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : evaluatedOptions.length - 1)) + } + } - // Handle the case where evaluatedOptions changes and the current selection is no longer valid - const isValueInOptions = useMemo(() => { - if (!effectiveValue || evaluatedOptions.length === 0) return false - return evaluatedOptions.some((opt) => getOptionValue(opt) === effectiveValue) - }, [effectiveValue, evaluatedOptions, getOptionValue]) + if (e.key === 'Enter' && open && highlightedIndex >= 0) { + e.preventDefault() + const selectedOption = evaluatedOptions[highlightedIndex] + if (selectedOption) { + handleSelect(getOptionValue(selectedOption)) + } + } + } + // Effects + useEffect(() => { + setHighlightedIndex((prev) => { + if (prev >= 0 && prev < evaluatedOptions.length) { + return prev + } + return -1 + }) + }, [evaluatedOptions]) + + // Scroll highlighted option into view + useEffect(() => { + if (highlightedIndex >= 0 && dropdownRef.current) { + const highlightedElement = dropdownRef.current.querySelector( + `[data-option-index="${highlightedIndex}"]` + ) + if (highlightedElement) { + highlightedElement.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }) + } + } + }, [highlightedIndex]) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element + if ( + inputRef.current && + !inputRef.current.contains(target) && + !target.closest('.absolute.top-full') + ) { + setOpen(false) + setHighlightedIndex(-1) + } + } + + if (open) { + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + } + }, [open]) + + // Display value + const displayValue = value?.toString() ?? '' + const selectedOption = evaluatedOptions.find((opt) => getOptionValue(opt) === value) + const selectedLabel = selectedOption ? getOptionLabel(selectedOption) : displayValue + const SelectedIcon = + selectedOption && typeof selectedOption === 'object' && 'icon' in selectedOption + ? (selectedOption.icon as React.ComponentType<{ className?: string }>) + : null + + // Render component return ( - + {/* Icon overlay */} + {SelectedIcon && ( +
+ +
+ )} + {/* Chevron button */} + + - setStoreValue(newValue) - } - }} - disabled={isPreview || disabled} - > - - - - - {evaluatedOptions.map((option) => ( - - {getOptionLabel(option)} - - ))} - - + {/* Dropdown */} + {open && ( +
+
+
+ {evaluatedOptions.length === 0 ? ( +
+ No options available. +
+ ) : ( + evaluatedOptions.map((option, index) => { + const optionValue = getOptionValue(option) + const optionLabel = getOptionLabel(option) + const OptionIcon = + typeof option === 'object' && 'icon' in option + ? (option.icon as React.ComponentType<{ className?: string }>) + : null + const isSelected = value === optionValue + const isHighlighted = index === highlightedIndex + + return ( +
handleSelect(optionValue)} + onMouseDown={(e) => { + e.preventDefault() + handleSelect(optionValue) + }} + onMouseEnter={() => setHighlightedIndex(index)} + className={cn( + 'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground', + isHighlighted && 'bg-accent text-accent-foreground' + )} + > + {OptionIcon && } + {optionLabel} + {isSelected && } +
+ ) + }) + )} +
+
+
+ )} + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/components/schedule-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/components/schedule-modal.tsx index c0f0a43c96..be4cd80dba 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/components/schedule-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/components/schedule-modal.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react' -import { format } from 'date-fns' import { Trash2, X } from 'lucide-react' import { Alert, AlertDescription } from '@/components/ui/alert' import { @@ -13,10 +12,8 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' -import { Calendar as CalendarComponent } from '@/components/ui/calendar' import { DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Select, SelectContent, @@ -54,8 +51,6 @@ export function ScheduleModal({ }: ScheduleModalProps) { // States for schedule configuration const [scheduleType, setScheduleType] = useSubBlockValue(blockId, 'scheduleType') - const [scheduleStartAt, setScheduleStartAt] = useSubBlockValue(blockId, 'scheduleStartAt') - const [scheduleTime, setScheduleTime] = useSubBlockValue(blockId, 'scheduleTime') const [minutesInterval, setMinutesInterval] = useSubBlockValue(blockId, 'minutesInterval') const [hourlyMinute, setHourlyMinute] = useSubBlockValue(blockId, 'hourlyMinute') const [dailyTime, setDailyTime] = useSubBlockValue(blockId, 'dailyTime') @@ -86,8 +81,6 @@ export function ScheduleModal({ // Capture all current values when modal opens const currentValues = { scheduleType: scheduleType || 'daily', - scheduleStartAt: scheduleStartAt || '', - scheduleTime: scheduleTime || '', minutesInterval: minutesInterval || '', hourlyMinute: hourlyMinute || '', dailyTime: dailyTime || '', @@ -111,8 +104,6 @@ export function ScheduleModal({ const currentValues = { scheduleType: scheduleType || 'daily', - scheduleStartAt: scheduleStartAt || '', - scheduleTime: scheduleTime || '', minutesInterval: minutesInterval || '', hourlyMinute: hourlyMinute || '', dailyTime: dailyTime || '', @@ -160,8 +151,6 @@ export function ScheduleModal({ isOpen, scheduleId, scheduleType, - scheduleStartAt, - scheduleTime, minutesInterval, hourlyMinute, dailyTime, @@ -188,8 +177,6 @@ export function ScheduleModal({ // Revert form values to initial values if (hasChanges) { setScheduleType(initialValues.scheduleType) - setScheduleStartAt(initialValues.scheduleStartAt) - setScheduleTime(initialValues.scheduleTime) setMinutesInterval(initialValues.minutesInterval) setHourlyMinute(initialValues.hourlyMinute) setDailyTime(initialValues.dailyTime) @@ -279,8 +266,6 @@ export function ScheduleModal({ // Update initial values to match current state const updatedValues = { scheduleType: scheduleType || 'daily', - scheduleStartAt: scheduleStartAt || '', - scheduleTime: scheduleTime || '', minutesInterval: minutesInterval || '', hourlyMinute: hourlyMinute || '', dailyTime: dailyTime || '', @@ -329,15 +314,6 @@ export function ScheduleModal({ setShowDeleteConfirm(true) } - // Helper to format a date for display - const formatDate = (date: string) => { - try { - return date ? format(new Date(date), 'PPP') : 'Select date' - } catch (_e) { - return 'Select date' - } - } - return ( <> @@ -359,46 +335,6 @@ export function ScheduleModal({ )}
- {/* Common date and time fields */} -
-
- - - - - - - setScheduleStartAt(date ? date.toISOString() : '')} - initialFocus - /> - - -
- -
- - -
-
- {/* Frequency selector */}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx index 65870360d8..33d9b1c192 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/schedule/schedule-config.tsx @@ -6,7 +6,7 @@ import { Dialog } from '@/components/ui/dialog' import { createLogger } from '@/lib/logs/console-logger' import { parseCronToHumanReadable } from '@/lib/schedules/utils' import { formatDateTime } from '@/lib/utils' -import { getWorkflowWithValues } from '@/stores/workflows' +import { getBlockWithValues, getWorkflowWithValues } from '@/stores/workflows' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -49,7 +49,6 @@ export function ScheduleConfig({ const workflowId = params.workflowId as string // Get workflow state from store - const setScheduleStatus = useWorkflowStore((state) => state.setScheduleStatus) // Get the schedule type from the block state const [scheduleType] = useSubBlockValue(blockId, 'scheduleType') @@ -58,12 +57,25 @@ export function ScheduleConfig({ // and expose the setter so we can update it const [_startWorkflow, setStartWorkflow] = useSubBlockValue(blockId, 'startWorkflow') + // Determine if this is a schedule trigger block vs starter block + const blockWithValues = getBlockWithValues(blockId) + const isScheduleTriggerBlock = blockWithValues?.type === 'schedule' + // Function to check if schedule exists in the database const checkSchedule = async () => { setIsLoading(true) try { // Check if there's a schedule for this workflow, passing the mode parameter - const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, { + // For schedule trigger blocks, include blockId to get the specific schedule + const url = new URL('/api/schedules', window.location.origin) + url.searchParams.set('workflowId', workflowId) + url.searchParams.set('mode', 'schedule') + + if (isScheduleTriggerBlock) { + url.searchParams.set('blockId', blockId) + } + + const response = await fetch(url.toString(), { // Add cache: 'no-store' to prevent caching of this request cache: 'no-store', headers: { @@ -82,16 +94,15 @@ export function ScheduleConfig({ setCronExpression(data.schedule.cronExpression) setTimezone(data.schedule.timezone || 'UTC') - // Set active schedule flag to true since we found an active schedule - setScheduleStatus(true) + // Note: We no longer set global schedule status from individual components + // The global schedule status should be managed by a higher-level component } else { setScheduleId(null) setNextRunAt(null) setLastRanAt(null) setCronExpression(null) - // Set active schedule flag to false since no schedule was found - setScheduleStatus(false) + // Note: We no longer set global schedule status from individual components } } } catch (error) { @@ -104,9 +115,8 @@ export function ScheduleConfig({ // Check for schedule on mount and when relevant dependencies change useEffect(() => { - // Only check for schedules when workflowId changes or modal opens - // Avoid checking on every scheduleType change to prevent excessive API calls - if (workflowId && (isModalOpen || refreshCounter > 0)) { + // Check for schedules when workflowId changes, modal opens, or on initial mount + if (workflowId) { checkSchedule() } @@ -160,23 +170,33 @@ export function ScheduleConfig({ setError(null) try { - // 1. First, update the startWorkflow value in SubBlock store to 'schedule' - setStartWorkflow('schedule') + // For starter blocks, update the startWorkflow value to 'schedule' + // For schedule trigger blocks, skip this step as startWorkflow is not needed + if (!isScheduleTriggerBlock) { + // 1. First, update the startWorkflow value in SubBlock store to 'schedule' + setStartWorkflow('schedule') + + // 2. Directly access and modify the SubBlock store to guarantee the value is set + const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId + if (!activeWorkflowId) { + setError('No active workflow found') + return false + } + + // Update the SubBlock store directly to ensure the value is set correctly + const subBlockStore = useSubBlockStore.getState() + subBlockStore.setValue(blockId, 'startWorkflow', 'schedule') + + // Give React time to process the state update + await new Promise((resolve) => setTimeout(resolve, 200)) + } - // 2. Directly access and modify the SubBlock store to guarantee the value is set const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId if (!activeWorkflowId) { setError('No active workflow found') return false } - // Update the SubBlock store directly to ensure the value is set correctly - const subBlockStore = useSubBlockStore.getState() - subBlockStore.setValue(blockId, 'startWorkflow', 'schedule') - - // Give React time to process the state update - await new Promise((resolve) => setTimeout(resolve, 200)) - // 3. Get the fully merged current state with updated values // This ensures we send the complete, correct workflow state to the backend const currentWorkflowWithValues = getWorkflowWithValues(activeWorkflowId) @@ -188,15 +208,24 @@ export function ScheduleConfig({ // 4. Make a direct API call instead of relying on sync // This gives us more control and better error handling logger.debug('Making direct API call to save schedule with complete state') + + // Prepare the request body + const requestBody: any = { + workflowId, + state: currentWorkflowWithValues.state, + } + + // For schedule trigger blocks, include the blockId + if (isScheduleTriggerBlock) { + requestBody.blockId = blockId + } + const response = await fetch('/api/schedules', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - workflowId, - state: currentWorkflowWithValues.state, - }), + body: JSON.stringify(requestBody), }) // Parse the response @@ -230,7 +259,7 @@ export function ScheduleConfig({ } // 6. Update the schedule status and trigger a workflow update - setScheduleStatus(true) + // Note: Global schedule status is managed at a higher level // 7. Tell the workflow store that the state has been saved const workflowStore = useWorkflowStore.getState() @@ -262,25 +291,29 @@ export function ScheduleConfig({ setIsDeleting(true) try { - // 1. First update the workflow state to disable scheduling - setStartWorkflow('manual') + // For starter blocks, update the startWorkflow value to 'manual' + // For schedule trigger blocks, skip this step as startWorkflow is not relevant + if (!isScheduleTriggerBlock) { + // 1. First update the workflow state to disable scheduling + setStartWorkflow('manual') - // 2. Directly update the SubBlock store to ensure the value is set - const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId - if (!activeWorkflowId) { - setError('No active workflow found') - return false + // 2. Directly update the SubBlock store to ensure the value is set + const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId + if (!activeWorkflowId) { + setError('No active workflow found') + return false + } + + // Update the store directly + const subBlockStore = useSubBlockStore.getState() + subBlockStore.setValue(blockId, 'startWorkflow', 'manual') + + // 3. Update the workflow store + const workflowStore = useWorkflowStore.getState() + workflowStore.triggerUpdate() + workflowStore.updateLastSaved() } - // Update the store directly - const subBlockStore = useSubBlockStore.getState() - subBlockStore.setValue(blockId, 'startWorkflow', 'manual') - - // 3. Update the workflow store - const workflowStore = useWorkflowStore.getState() - workflowStore.triggerUpdate() - workflowStore.updateLastSaved() - // 4. Make the DELETE API call to remove the schedule const response = await fetch(`/api/schedules/${scheduleId}`, { method: 'DELETE', @@ -299,7 +332,7 @@ export function ScheduleConfig({ setCronExpression(null) // 6. Update schedule status and refresh UI - setScheduleStatus(false) + // Note: Global schedule status is managed at a higher level setRefreshCounter((prev) => prev + 1) return true diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx index 825db4c761..2de92c4a9b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/webhook.tsx @@ -14,7 +14,6 @@ import { import { Button } from '@/components/ui/button' import { createLogger } from '@/lib/logs/console-logger' import { useSubBlockStore } from '@/stores/workflows/subblock/store' -import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { useSubBlockValue } from '../../hooks/use-sub-block-value' import { ToolCredentialSelector } from '../tool-input/components/tool-credential-selector' import { WebhookModal } from './components/webhook-modal' @@ -314,8 +313,7 @@ export function WebhookConfig({ const [isLoading, setIsLoading] = useState(false) const [gmailCredentialId, setGmailCredentialId] = useState('') - // Get workflow store function to update webhook status - const setWebhookStatus = useWorkflowStore((state) => state.setWebhookStatus) + // No need to manage webhook status separately - it's determined by having provider + path // Get the webhook provider from the block state const [storeWebhookProvider, setWebhookProvider] = useSubBlockValue(blockId, 'webhookProvider') @@ -323,6 +321,9 @@ export function WebhookConfig({ // Store the webhook path const [storeWebhookPath, setWebhookPath] = useSubBlockValue(blockId, 'webhookPath') + // Don't auto-generate webhook paths - only create them when user actually configures a webhook + // This prevents the "Active Webhook" badge from showing on unconfigured blocks + // Store provider-specific configuration const [storeProviderConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig') @@ -331,16 +332,132 @@ export function WebhookConfig({ const webhookPath = propValue?.webhookPath ?? storeWebhookPath const providerConfig = propValue?.providerConfig ?? storeProviderConfig - // Reset provider config when provider changes + // Store the actual provider from the database + const [actualProvider, setActualProvider] = useState(null) + + // Track the previous provider to detect changes + const [previousProvider, setPreviousProvider] = useState(null) + + // Handle provider changes - clear webhook data when switching providers + useEffect(() => { + // Skip on initial load or if no provider is set + if (!webhookProvider || !previousProvider) { + setPreviousProvider(webhookProvider) + return + } + + // If the provider has changed, clear all webhook-related data + if (webhookProvider !== previousProvider) { + // IMPORTANT: Store the current webhook ID BEFORE clearing it + const currentWebhookId = webhookId + + logger.info('Webhook provider changed, clearing webhook data', { + from: previousProvider, + to: webhookProvider, + blockId, + webhookId: currentWebhookId, + }) + + // If there's an existing webhook, delete it from the database + const deleteExistingWebhook = async () => { + if (currentWebhookId && !isPreview) { + try { + logger.info('Deleting existing webhook due to provider change', { + webhookId: currentWebhookId, + oldProvider: previousProvider, + newProvider: webhookProvider, + }) + + const response = await fetch(`/api/webhooks/${currentWebhookId}`, { + method: 'DELETE', + }) + + if (!response.ok) { + const errorData = await response.json() + logger.error('Failed to delete existing webhook', { + webhookId: currentWebhookId, + error: errorData.error, + }) + } else { + logger.info('Successfully deleted existing webhook', { webhookId: currentWebhookId }) + + const store = useSubBlockStore.getState() + const workflowValues = store.workflowValues[workflowId] || {} + const blockValues = { ...workflowValues[blockId] } + + // Clear webhook-related fields + blockValues.webhookPath = undefined + blockValues.providerConfig = undefined + + // Update the store with the cleaned block values + useSubBlockStore.setState({ + workflowValues: { + ...workflowValues, + [workflowId]: { + ...workflowValues, + [blockId]: blockValues, + }, + }, + }) + + logger.info('Cleared webhook data from store after successful deletion', { blockId }) + } + } catch (error: any) { + logger.error('Error deleting existing webhook', { + webhookId: currentWebhookId, + error: error.message, + }) + } + } + } + + // Clear webhook fields FIRST to make badge disappear immediately + // Then delete from database to prevent the webhook check useEffect from restoring the path + + // IMPORTANT: Clear webhook connection data FIRST + // This prevents the webhook check useEffect from finding and restoring the webhook + setWebhookId(null) + setActualProvider(null) + + // Clear provider config + setProviderConfig({}) + + // Clear component state + setError(null) + setGmailCredentialId('') + + // Note: Store will be cleared AFTER successful database deletion + // This ensures store and database stay perfectly in sync + + // Update previous provider to the new provider + setPreviousProvider(webhookProvider) + + // Delete existing webhook AFTER clearing the path to prevent race condition + // The webhook check useEffect won't restore the path if we clear it first + // Execute deletion asynchronously but don't block the UI + + ;(async () => { + await deleteExistingWebhook() + })() + } + }, [webhookProvider, previousProvider, blockId, webhookId, isPreview]) + + // Reset provider config when provider changes (legacy effect - keeping for safety) useEffect(() => { if (webhookProvider) { // Reset the provider config when the provider changes setProviderConfig({}) - } - }, [webhookProvider, setProviderConfig]) - // Store the actual provider from the database - const [actualProvider, setActualProvider] = useState(null) + // Clear webhook ID and actual provider when switching providers + // This ensures the webhook status is properly reset + if (webhookProvider !== actualProvider) { + setWebhookId(null) + setActualProvider(null) + } + + // Provider config is reset - webhook status will be determined by provider + path existence + } + }, [webhookProvider, webhookId, actualProvider]) // Check if webhook exists in the database useEffect(() => { @@ -353,18 +470,17 @@ export function WebhookConfig({ const checkWebhook = async () => { setIsLoading(true) try { - // Check if there's a webhook for this workflow - const response = await fetch(`/api/webhooks?workflowId=${workflowId}`) + // Check if there's a webhook for this specific block + // Always include blockId - every webhook should be associated with a specific block + const response = await fetch(`/api/webhooks?workflowId=${workflowId}&blockId=${blockId}`) if (response.ok) { const data = await response.json() if (data.webhooks && data.webhooks.length > 0) { const webhook = data.webhooks[0].webhook setWebhookId(webhook.id) - // Update the provider in the block state if it's different - if (webhook.provider && webhook.provider !== webhookProvider) { - setWebhookProvider(webhook.provider) - } + // Don't automatically update the provider - let user control it + // The user should be able to change providers even when a webhook exists // Store the actual provider from the database setActualProvider(webhook.provider) @@ -374,14 +490,22 @@ export function WebhookConfig({ setWebhookPath(webhook.path) } - // Set active webhook flag to true since we found an active webhook - setWebhookStatus(true) + // Webhook found - status will be determined by provider + path existence } else { setWebhookId(null) setActualProvider(null) - // Set active webhook flag to false since no webhook was found - setWebhookStatus(false) + // IMPORTANT: Clear stale webhook data from store when no webhook found in database + // This ensures the reactive badge status updates correctly on page refresh + if (webhookPath) { + setWebhookPath('') + logger.info('Cleared stale webhook path on page refresh - no webhook in database', { + blockId, + clearedPath: webhookPath, + }) + } + + // No webhook found - reactive blockWebhookStatus will now be false } } } catch (error) { @@ -392,15 +516,7 @@ export function WebhookConfig({ } checkWebhook() - }, [ - webhookPath, - webhookProvider, - workflowId, - setWebhookPath, - setWebhookProvider, - setWebhookStatus, - isPreview, - ]) + }, [workflowId, blockId, isPreview]) // Removed webhookPath dependency to prevent race condition with provider changes const handleOpenModal = () => { if (isPreview || disabled) return @@ -443,6 +559,7 @@ export function WebhookConfig({ }, body: JSON.stringify({ workflowId, + blockId, path, provider: webhookProvider || 'generic', providerConfig: finalConfig, @@ -459,13 +576,20 @@ export function WebhookConfig({ } const data = await response.json() - setWebhookId(data.webhook.id) + const savedWebhookId = data.webhook.id + setWebhookId(savedWebhookId) + + logger.info('Webhook saved successfully', { + webhookId: savedWebhookId, + provider: webhookProvider, + path, + blockId, + }) // Update the actual provider after saving setActualProvider(webhookProvider || 'generic') - // Set active webhook flag to true after successfully saving - setWebhookStatus(true) + // Webhook saved successfully - status will be determined by provider + path existence return true } catch (error: any) { @@ -504,7 +628,7 @@ export function WebhookConfig({ // Remove webhook-related fields blockValues.webhookProvider = undefined blockValues.providerConfig = undefined - blockValues.webhookPath = '' + blockValues.webhookPath = undefined // Update the store with the cleaned block values store.setValue(blockId, 'startWorkflow', 'manual') @@ -522,8 +646,7 @@ export function WebhookConfig({ setWebhookId(null) setActualProvider(null) - // Set active webhook flag to false - setWebhookStatus(false) + // Webhook deleted - status will be determined by provider + path existence handleCloseModal() return true diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 2a3cd42cb5..c6d9890e6d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -7,12 +7,13 @@ import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { parseCronToHumanReadable } from '@/lib/schedules/utils' -import { cn, formatDateTime, validateName } from '@/lib/utils' +import { cn, validateName } from '@/lib/utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useExecutionStore } from '@/stores/execution/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { mergeSubblockState } from '@/stores/workflows/utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { ActionBar } from './components/action-bar/action-bar' @@ -67,7 +68,17 @@ export function WorkflowBlock({ id, data }: NodeProps) { ) const isWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false) const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0) - const hasActiveWebhook = useWorkflowStore((state) => state.hasActiveWebhook ?? false) + // Get per-block webhook status by checking if webhook is configured + const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) + + const hasWebhookProvider = useSubBlockStore( + (state) => state.workflowValues[activeWorkflowId || '']?.[id]?.webhookProvider + ) + const hasWebhookPath = useSubBlockStore( + (state) => state.workflowValues[activeWorkflowId || '']?.[id]?.webhookPath + ) + const blockWebhookStatus = !!(hasWebhookProvider && hasWebhookPath) + const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false) // Collaborative workflow actions @@ -89,6 +100,11 @@ export function WorkflowBlock({ id, data }: NodeProps) { const params = useParams() const currentWorkflowId = params.workflowId as string + // Check if this is a starter block or trigger block + const isStarterBlock = type === 'starter' + const isTriggerBlock = config.category === 'triggers' + const isWebhookTriggerBlock = type === 'webhook' + const reactivateSchedule = async (scheduleId: string) => { try { const response = await fetch(`/api/schedules/${scheduleId}`, { @@ -112,13 +128,42 @@ export function WorkflowBlock({ id, data }: NodeProps) { } } + const disableSchedule = async (scheduleId: string) => { + try { + const response = await fetch(`/api/schedules/${scheduleId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ action: 'disable' }), + }) + + if (response.ok) { + // Refresh schedule info to show updated status + if (currentWorkflowId) { + fetchScheduleInfo(currentWorkflowId) + } + } else { + console.error('Failed to disable schedule') + } + } catch (error) { + console.error('Error disabling schedule:', error) + } + } + const fetchScheduleInfo = async (workflowId: string) => { if (!workflowId) return try { setIsLoadingScheduleInfo(true) - const response = await fetch(`/api/schedules?workflowId=${workflowId}&mode=schedule`, { + // For schedule trigger blocks, always include the blockId parameter + const url = new URL('/api/schedules', window.location.origin) + url.searchParams.set('workflowId', workflowId) + url.searchParams.set('mode', 'schedule') + url.searchParams.set('blockId', id) // Always include blockId for schedule blocks + + const response = await fetch(url.toString(), { cache: 'no-store', headers: { 'Cache-Control': 'no-cache', @@ -185,48 +230,25 @@ export function WorkflowBlock({ id, data }: NodeProps) { } useEffect(() => { - if (type === 'starter' && currentWorkflowId) { + if (type === 'schedule' && currentWorkflowId) { fetchScheduleInfo(currentWorkflowId) } else { setScheduleInfo(null) - setIsLoadingScheduleInfo(false) // Reset loading state when not a starter block + setIsLoadingScheduleInfo(false) // Reset loading state when not a schedule block } // Cleanup function to reset loading state when component unmounts or workflow changes return () => { setIsLoadingScheduleInfo(false) } - }, [type, currentWorkflowId]) + }, [isStarterBlock, isTriggerBlock, type, currentWorkflowId, lastUpdate]) // Get webhook information for the tooltip useEffect(() => { - if (type === 'starter' && hasActiveWebhook) { - const fetchWebhookInfo = async () => { - try { - const workflowId = useWorkflowRegistry.getState().activeWorkflowId - if (!workflowId) return - - const response = await fetch(`/api/webhooks?workflowId=${workflowId}`) - if (response.ok) { - const data = await response.json() - if (data.webhooks?.[0]?.webhook) { - const webhook = data.webhooks[0].webhook - setWebhookInfo({ - webhookPath: webhook.path || '', - provider: webhook.provider || 'generic', - }) - } - } - } catch (error) { - console.error('Error fetching webhook info:', error) - } - } - - fetchWebhookInfo() - } else if (!hasActiveWebhook) { + if (!blockWebhookStatus) { setWebhookInfo(null) } - }, [type, hasActiveWebhook]) + }, [blockWebhookStatus]) // Update node internals when handles change useEffect(() => { @@ -404,9 +426,8 @@ export function WorkflowBlock({ id, data }: NodeProps) { } } - // Check if this is a starter block and has active schedule or webhook - const isStarterBlock = type === 'starter' - const showWebhookIndicator = isStarterBlock && hasActiveWebhook + // Check webhook indicator + const showWebhookIndicator = (isStarterBlock || isWebhookTriggerBlock) && blockWebhookStatus const getProviderName = (providerId: string): string => { const providers: Record = { @@ -422,7 +443,8 @@ export function WorkflowBlock({ id, data }: NodeProps) { return providers[providerId] || 'Webhook' } - const shouldShowScheduleBadge = isStarterBlock && !isLoadingScheduleInfo && scheduleInfo !== null + const shouldShowScheduleBadge = + type === 'schedule' && !isLoadingScheduleInfo && scheduleInfo !== null const userPermissions = useUserPermissionsContext() return ( @@ -447,15 +469,18 @@ export function WorkflowBlock({ id, data }: NodeProps) { )} - + {/* Connection Blocks - Don't show for trigger blocks or starter blocks */} + {config.category !== 'triggers' && type !== 'starter' && ( + + )} - {/* Input Handle - Don't show for starter blocks */} - {type !== 'starter' && ( + {/* Input Handle - Don't show for trigger blocks or starter blocks */} + {config.category !== 'triggers' && type !== 'starter' && ( ) { reactivateSchedule(scheduleInfo.id!) + scheduleInfo?.id + ? scheduleInfo.isDisabled + ? () => reactivateSchedule(scheduleInfo.id!) + : () => disableSchedule(scheduleInfo.id!) : undefined } > @@ -570,32 +597,12 @@ export function WorkflowBlock({ id, data }: NodeProps) { - {scheduleInfo ? ( - <> -

{scheduleInfo.scheduleTiming}

- {scheduleInfo.isDisabled && ( -

- This schedule is currently disabled due to consecutive failures. Click the - badge to reactivate it. -

- )} - {scheduleInfo.nextRunAt && !scheduleInfo.isDisabled && ( -

- Next run:{' '} - {formatDateTime(new Date(scheduleInfo.nextRunAt), scheduleInfo.timezone)} -

- )} - {scheduleInfo.lastRanAt && ( -

- Last run:{' '} - {formatDateTime(new Date(scheduleInfo.lastRanAt), scheduleInfo.timezone)} -

- )} - - ) : ( -

- This workflow is running on a schedule. + {scheduleInfo?.isDisabled ? ( +

+ This schedule is currently disabled. Click the badge to reactivate it.

+ ) : ( +

Click the badge to disable this schedule.

)}
@@ -825,8 +832,8 @@ export function WorkflowBlock({ id, data }: NodeProps) { isValidConnection={(connection) => connection.target !== id} /> - {/* Error Handle - Don't show for starter blocks */} - {type !== 'starter' && ( + {/* Error Handle - Don't show for trigger blocks or starter blocks */} + {config.category !== 'triggers' && type !== 'starter' && ( => { // Use the mergeSubblockState utility to get all block states const mergedStates = mergeSubblockState(blocks) - const currentBlockStates = Object.entries(mergedStates).reduce( + + // Filter out trigger blocks for manual execution + const filteredStates = Object.entries(mergedStates).reduce( + (acc, [id, block]) => { + const blockConfig = getBlock(block.type) + const isTriggerBlock = blockConfig?.category === 'triggers' + + // Skip trigger blocks during manual execution + if (!isTriggerBlock) { + acc[id] = block + } + return acc + }, + {} as typeof mergedStates + ) + + const currentBlockStates = Object.entries(filteredStates).reduce( (acc, [id, block]) => { acc[id] = Object.entries(block.subBlocks).reduce( (subAcc, [key, subBlock]) => { @@ -453,8 +470,23 @@ export function useWorkflowExecution() { {} as Record ) - // Create serialized workflow - const workflow = new Serializer().serializeWorkflow(mergedStates, edges, loops, parallels) + // Filter edges to exclude connections to/from trigger blocks + const triggerBlockIds = Object.keys(mergedStates).filter((id) => { + const blockConfig = getBlock(mergedStates[id].type) + return blockConfig?.category === 'triggers' + }) + + const filteredEdges = edges.filter( + (edge) => !triggerBlockIds.includes(edge.source) && !triggerBlockIds.includes(edge.target) + ) + + // Create serialized workflow with filtered blocks and edges + const workflow = new Serializer().serializeWorkflow( + filteredStates, + filteredEdges, + loops, + parallels + ) // Determine if this is a chat execution const isChatExecution = diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts index 73cec39ce1..23faa5206e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts @@ -1,4 +1,5 @@ import { createLogger } from '@/lib/logs/console-logger' +import { getBlock } from '@/blocks' const logger = createLogger('WorkflowUtils') @@ -549,14 +550,25 @@ export const analyzeWorkflowGraph = ( const outDegreeValue = (adjacencyList.get(blockId) || []).length const block = blocks[blockId] - if (inDegreeValue === 0 && outDegreeValue === 0 && block.type !== 'starter') { + const blockConfig = getBlock(block.type) + const isTriggerBlock = blockConfig?.category === 'triggers' + + if ( + inDegreeValue === 0 && + outDegreeValue === 0 && + block.type !== 'starter' && + !isTriggerBlock + ) { orphanedBlocks.add(blockId) } }) const queue: string[] = [] inDegree.forEach((degree, blockId) => { - if (degree === 0 || blocks[blockId].type === 'starter') { + const blockConfig = getBlock(blocks[blockId].type) + const isTriggerBlock = blockConfig?.category === 'triggers' + + if (degree === 0 || blocks[blockId].type === 'starter' || isTriggerBlock) { queue.push(blockId) blockLayers.set(blockId, 0) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 0b1e45a4e0..c3d84431aa 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -1080,6 +1080,16 @@ const WorkflowContent = React.memo(() => { if (!sourceNode || !targetNode) return + // Prevent incoming connections to trigger blocks (webhook, schedule, etc.) + if (targetNode.data?.config?.category === 'triggers') { + return + } + + // Prevent incoming connections to starter blocks (still keep separate for backward compatibility) + if (targetNode.data?.type === 'starter') { + return + } + // Get parent information (handle container start node case) const sourceParentId = sourceNode.parentId || diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/search-modal/search-modal.tsx index d545d38067..420a71e103 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/search-modal/search-modal.tsx @@ -143,7 +143,10 @@ export function SearchModal({ const allBlocks = getAllBlocks() return allBlocks .filter( - (block) => block.type !== 'starter' && !block.hideFromToolbar && block.category === 'blocks' + (block) => + block.type !== 'starter' && + !block.hideFromToolbar && + (block.category === 'blocks' || block.category === 'triggers') ) .map( (block): BlockItem => ({ @@ -222,7 +225,7 @@ export function SearchModal({ name: block.name, icon: block.icon, href: block.docsLink, - type: block.category === 'blocks' ? 'block' : 'tool', + type: block.category === 'blocks' || block.category === 'triggers' ? 'block' : 'tool', }) } }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx index e1b7965fb0..678d3c7b4b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx @@ -25,7 +25,7 @@ interface BlockItem { export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: ToolbarProps) { const [searchQuery, setSearchQuery] = useState('') - const { regularBlocks, specialBlocks, tools } = useMemo(() => { + const { regularBlocks, specialBlocks, tools, triggers } = useMemo(() => { const allBlocks = getAllBlocks() // Filter blocks based on search query @@ -39,9 +39,10 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: ) }) - // Separate regular blocks (category: 'blocks') and tools (category: 'tools') + // Separate blocks by category: 'blocks', 'tools', and 'triggers' const regularBlockConfigs = filteredBlocks.filter((block) => block.category === 'blocks') const toolConfigs = filteredBlocks.filter((block) => block.category === 'tools') + const triggerConfigs = filteredBlocks.filter((block) => block.category === 'triggers') // Create regular block items and sort alphabetically const regularBlockItems: BlockItem[] = regularBlockConfigs @@ -75,6 +76,16 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: // Sort special blocks alphabetically specialBlockItems.sort((a, b) => a.name.localeCompare(b.name)) + // Create trigger block items and sort alphabetically + const triggerBlockItems: BlockItem[] = triggerConfigs + .map((block) => ({ + name: block.name, + type: block.type, + config: block, + isCustom: false, + })) + .sort((a, b) => a.name.localeCompare(b.name)) + // Sort tools alphabetically toolConfigs.sort((a, b) => a.name.localeCompare(b.name)) @@ -82,6 +93,7 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: regularBlocks: regularBlockItems, specialBlocks: specialBlockItems, tools: toolConfigs, + triggers: triggerBlockItems, } }, [searchQuery]) @@ -127,6 +139,15 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: return null })} + {/* Triggers Section */} + {triggers.map((trigger) => ( + + ))} + {/* Tools Section */} {tools.map((tool) => ( diff --git a/apps/sim/blocks/blocks/schedule.ts b/apps/sim/blocks/blocks/schedule.ts new file mode 100644 index 0000000000..fe1af83378 --- /dev/null +++ b/apps/sim/blocks/blocks/schedule.ts @@ -0,0 +1,116 @@ +import { ScheduleIcon } from '@/components/icons' +import type { BlockConfig } from '../types' + +export const ScheduleBlock: BlockConfig = { + type: 'schedule', + name: 'Schedule', + description: 'Trigger workflow execution on a schedule', + longDescription: + 'Configure automated workflow execution with flexible timing options. Set up recurring workflows that run at specific intervals or times.', + category: 'triggers', + bgColor: '#7B68EE', + icon: ScheduleIcon, + + subBlocks: [ + // Schedule configuration status display + { + id: 'scheduleConfig', + title: 'Schedule Status', + type: 'schedule-config', + layout: 'full', + }, + // Hidden fields for schedule configuration (used by the modal only) + { + id: 'scheduleType', + title: 'Frequency', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Every X Minutes', id: 'minutes' }, + { label: 'Hourly', id: 'hourly' }, + { label: 'Daily', id: 'daily' }, + { label: 'Weekly', id: 'weekly' }, + { label: 'Monthly', id: 'monthly' }, + { label: 'Custom Cron', id: 'custom' }, + ], + value: () => 'daily', + hidden: true, + }, + { + id: 'minutesInterval', + type: 'short-input', + hidden: true, + }, + { + id: 'hourlyMinute', + type: 'short-input', + hidden: true, + }, + { + id: 'dailyTime', + type: 'short-input', + hidden: true, + }, + { + id: 'weeklyDay', + type: 'dropdown', + hidden: true, + options: [ + { label: 'Monday', id: 'MON' }, + { label: 'Tuesday', id: 'TUE' }, + { label: 'Wednesday', id: 'WED' }, + { label: 'Thursday', id: 'THU' }, + { label: 'Friday', id: 'FRI' }, + { label: 'Saturday', id: 'SAT' }, + { label: 'Sunday', id: 'SUN' }, + ], + value: () => 'MON', + }, + { + id: 'weeklyDayTime', + type: 'short-input', + hidden: true, + }, + { + id: 'monthlyDay', + type: 'short-input', + hidden: true, + }, + { + id: 'monthlyTime', + type: 'short-input', + hidden: true, + }, + { + id: 'cronExpression', + type: 'short-input', + hidden: true, + }, + { + id: 'timezone', + type: 'dropdown', + hidden: true, + options: [ + { label: 'UTC', id: 'UTC' }, + { label: 'US Eastern (UTC-4)', id: 'America/New_York' }, + { label: 'US Central (UTC-5)', id: 'America/Chicago' }, + { label: 'US Mountain (UTC-6)', id: 'America/Denver' }, + { label: 'US Pacific (UTC-7)', id: 'America/Los_Angeles' }, + { label: 'London (UTC+1)', id: 'Europe/London' }, + { label: 'Paris (UTC+2)', id: 'Europe/Paris' }, + { label: 'Singapore (UTC+8)', id: 'Asia/Singapore' }, + { label: 'Tokyo (UTC+9)', id: 'Asia/Tokyo' }, + { label: 'Sydney (UTC+10)', id: 'Australia/Sydney' }, + ], + value: () => 'UTC', + }, + ], + + tools: { + access: [], // No external tools needed + }, + + inputs: {}, // No inputs - schedule triggers initiate workflows + + outputs: {}, // No outputs - schedule triggers initiate workflow execution +} diff --git a/apps/sim/blocks/blocks/starter.ts b/apps/sim/blocks/blocks/starter.ts index 0af83aefee..412b670698 100644 --- a/apps/sim/blocks/blocks/starter.ts +++ b/apps/sim/blocks/blocks/starter.ts @@ -5,8 +5,7 @@ export const StarterBlock: BlockConfig = { type: 'starter', name: 'Starter', description: 'Start workflow', - longDescription: - 'Initiate your workflow manually, on a schedule, or via webhook triggers. Configure flexible execution patterns with customizable timing options and webhook security.', + longDescription: 'Initiate your workflow manually with optional structured input for API calls.', category: 'blocks', bgColor: '#2FB3FF', icon: StartIcon, @@ -19,8 +18,7 @@ export const StarterBlock: BlockConfig = { layout: 'full', options: [ { label: 'Run manually', id: 'manual' }, - { label: 'On webhook call', id: 'webhook' }, - { label: 'On schedule', id: 'schedule' }, + { label: 'Chat', id: 'chat' }, ], value: () => 'manual', }, @@ -33,148 +31,6 @@ export const StarterBlock: BlockConfig = { mode: 'advanced', condition: { field: 'startWorkflow', value: 'manual' }, }, - // Webhook configuration - { - id: 'webhookProvider', - title: 'Webhook Provider', - type: 'dropdown', - layout: 'full', - options: [ - { label: 'Slack', id: 'slack' }, - { label: 'Gmail', id: 'gmail' }, - { label: 'Airtable', id: 'airtable' }, - { label: 'Telegram', id: 'telegram' }, - { label: 'Generic', id: 'generic' }, - // { label: 'WhatsApp', id: 'whatsapp' }, - // { label: 'GitHub', id: 'github' }, - // { label: 'Discord', id: 'discord' }, - // { label: 'Stripe', id: 'stripe' }, - ], - value: () => 'generic', - condition: { field: 'startWorkflow', value: 'webhook' }, - }, - { - id: 'webhookConfig', - title: 'Webhook Configuration', - type: 'webhook-config', - layout: 'full', - condition: { field: 'startWorkflow', value: 'webhook' }, - }, - // Schedule configuration status display - { - id: 'scheduleConfig', - title: 'Schedule Status', - type: 'schedule-config', - layout: 'full', - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - // Hidden fields for schedule configuration (used by the modal only) - { - id: 'scheduleType', - title: 'Frequency', - type: 'dropdown', - layout: 'full', - options: [ - { label: 'Every X Minutes', id: 'minutes' }, - { label: 'Hourly', id: 'hourly' }, - { label: 'Daily', id: 'daily' }, - { label: 'Weekly', id: 'weekly' }, - { label: 'Monthly', id: 'monthly' }, - { label: 'Custom Cron', id: 'custom' }, - ], - value: () => 'daily', - hidden: true, - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'scheduleStartAt', - type: 'date-input', - hidden: true, - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'scheduleTime', - type: 'time-input', - hidden: true, - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'minutesInterval', - type: 'short-input', - hidden: true, - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'hourlyMinute', - type: 'short-input', - hidden: true, - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'dailyTime', - type: 'short-input', - hidden: true, - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'weeklyDay', - type: 'dropdown', - hidden: true, - options: [ - { label: 'Monday', id: 'MON' }, - { label: 'Tuesday', id: 'TUE' }, - { label: 'Wednesday', id: 'WED' }, - { label: 'Thursday', id: 'THU' }, - { label: 'Friday', id: 'FRI' }, - { label: 'Saturday', id: 'SAT' }, - { label: 'Sunday', id: 'SUN' }, - ], - value: () => 'MON', - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'weeklyDayTime', - type: 'short-input', - hidden: true, - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'monthlyDay', - type: 'short-input', - hidden: true, - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'monthlyTime', - type: 'short-input', - hidden: true, - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'cronExpression', - type: 'short-input', - hidden: true, - condition: { field: 'startWorkflow', value: 'schedule' }, - }, - { - id: 'timezone', - type: 'dropdown', - hidden: true, - options: [ - { label: 'UTC', id: 'UTC' }, - { label: 'US Eastern (UTC-4)', id: 'America/New_York' }, - { label: 'US Central (UTC-5)', id: 'America/Chicago' }, - { label: 'US Mountain (UTC-6)', id: 'America/Denver' }, - { label: 'US Pacific (UTC-7)', id: 'America/Los_Angeles' }, - { label: 'London (UTC+1)', id: 'Europe/London' }, - { label: 'Paris (UTC+2)', id: 'Europe/Paris' }, - { label: 'Singapore (UTC+8)', id: 'Asia/Singapore' }, - { label: 'Tokyo (UTC+9)', id: 'Asia/Tokyo' }, - { label: 'Sydney (UTC+10)', id: 'Australia/Sydney' }, - ], - value: () => 'UTC', - condition: { field: 'startWorkflow', value: 'schedule' }, - }, ], tools: { access: [], diff --git a/apps/sim/blocks/blocks/webhook.ts b/apps/sim/blocks/blocks/webhook.ts new file mode 100644 index 0000000000..2f361e4f5a --- /dev/null +++ b/apps/sim/blocks/blocks/webhook.ts @@ -0,0 +1,92 @@ +import { + AirtableIcon, + DiscordIcon, + GithubIcon, + GmailIcon, + SignalIcon, + SlackIcon, + StripeIcon, + TelegramIcon, + WebhookIcon, + WhatsAppIcon, +} from '@/components/icons' +import type { BlockConfig } from '../types' + +const getWebhookProviderIcon = (provider: string) => { + const iconMap: Record> = { + slack: SlackIcon, + gmail: GmailIcon, + airtable: AirtableIcon, + telegram: TelegramIcon, + generic: SignalIcon, + whatsapp: WhatsAppIcon, + github: GithubIcon, + discord: DiscordIcon, + stripe: StripeIcon, + } + + return iconMap[provider.toLowerCase()] +} + +export const WebhookBlock: BlockConfig = { + type: 'webhook', + name: 'Webhook', + description: 'Trigger workflow execution from external webhooks', + category: 'triggers', + icon: WebhookIcon, + bgColor: '#10B981', // Green color for triggers + + subBlocks: [ + { + id: 'webhookProvider', + title: 'Webhook Provider', + type: 'dropdown', + layout: 'full', + options: [ + 'slack', + 'gmail', + 'airtable', + 'telegram', + 'generic', + 'whatsapp', + 'github', + 'discord', + 'stripe', + ].map((provider) => { + const providerLabels = { + slack: 'Slack', + gmail: 'Gmail', + airtable: 'Airtable', + telegram: 'Telegram', + generic: 'Generic', + whatsapp: 'WhatsApp', + github: 'GitHub', + discord: 'Discord', + stripe: 'Stripe', + } + + const icon = getWebhookProviderIcon(provider) + return { + label: providerLabels[provider as keyof typeof providerLabels], + id: provider, + ...(icon && { icon }), + } + }), + value: () => 'generic', + }, + { + id: 'webhookConfig', + title: 'Webhook Configuration', + type: 'webhook-config', + layout: 'full', + }, + ], + + tools: { + access: [], // No external tools needed + }, + + inputs: {}, // No inputs - webhook triggers are pure input sources + + outputs: {}, // No outputs - webhook data is injected directly into workflow context +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 467071b1be..eabee94198 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -45,6 +45,7 @@ import { RedditBlock } from '@/blocks/blocks/reddit' import { ResponseBlock } from '@/blocks/blocks/response' import { RouterBlock } from '@/blocks/blocks/router' import { S3Block } from '@/blocks/blocks/s3' +import { ScheduleBlock } from '@/blocks/blocks/schedule' import { SerperBlock } from '@/blocks/blocks/serper' import { SlackBlock } from '@/blocks/blocks/slack' import { StagehandBlock } from '@/blocks/blocks/stagehand' @@ -59,6 +60,7 @@ import { TwilioSMSBlock } from '@/blocks/blocks/twilio' import { TypeformBlock } from '@/blocks/blocks/typeform' import { VisionBlock } from '@/blocks/blocks/vision' import { WealthboxBlock } from '@/blocks/blocks/wealthbox' +import { WebhookBlock } from '@/blocks/blocks/webhook' import { WhatsAppBlock } from '@/blocks/blocks/whatsapp' import { WorkflowBlock } from '@/blocks/blocks/workflow' import { XBlock } from '@/blocks/blocks/x' @@ -108,6 +110,7 @@ export const registry: Record = { reddit: RedditBlock, response: ResponseBlock, router: RouterBlock, + schedule: ScheduleBlock, s3: S3Block, serper: SerperBlock, stagehand: StagehandBlock, @@ -123,6 +126,7 @@ export const registry: Record = { typeform: TypeformBlock, vision: VisionBlock, wealthbox: WealthboxBlock, + webhook: WebhookBlock, whatsapp: WhatsAppBlock, workflow: WorkflowBlock, x: XBlock, @@ -132,7 +136,7 @@ export const registry: Record = { // Helper functions to access the registry export const getBlock = (type: string): BlockConfig | undefined => registry[type] -export const getBlocksByCategory = (category: 'blocks' | 'tools'): BlockConfig[] => +export const getBlocksByCategory = (category: 'blocks' | 'tools' | 'triggers'): BlockConfig[] => Object.values(registry).filter((block) => block.category === category) export const getAllBlockTypes = (): string[] => Object.keys(registry) diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index d68c2158ff..bcc7a75dd5 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -7,7 +7,7 @@ export type ParamType = 'string' | 'number' | 'boolean' | 'json' export type PrimitiveValueType = 'string' | 'number' | 'boolean' | 'json' | 'any' // Block classification -export type BlockCategory = 'blocks' | 'tools' +export type BlockCategory = 'blocks' | 'tools' | 'triggers' // SubBlock types export type SubBlockType = diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 36eed84bdc..df94a90ca3 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2988,3 +2988,41 @@ export function WealthboxIcon(props: SVGProps) { ) } + +export function WebhookIcon(props: SVGProps) { + return ( + + + + + ) +} + +export function ScheduleIcon(props: SVGProps) { + return ( + + + + + + + ) +} diff --git a/apps/sim/components/ui/tag-dropdown.tsx b/apps/sim/components/ui/tag-dropdown.tsx index 40c9bb66a7..df2f613df8 100644 --- a/apps/sim/components/ui/tag-dropdown.tsx +++ b/apps/sim/components/ui/tag-dropdown.tsx @@ -184,19 +184,33 @@ export const TagDropdown: React.FC = ({ } else if (Object.keys(blockConfig.outputs).length === 0) { // Handle blocks with no outputs (like starter) - check for custom input fields if (sourceBlock.type === 'starter') { - // Check for custom input format fields - const inputFormatValue = useSubBlockStore + // Check what start workflow mode is selected + const startWorkflowValue = useSubBlockStore .getState() - .getValue(activeSourceBlockId, 'inputFormat') + .getValue(activeSourceBlockId, 'startWorkflow') - if (inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0) { - // Use custom input fields if they exist - blockTags = inputFormatValue - .filter((field: any) => field.name && field.name.trim() !== '') - .map((field: any) => `${normalizedBlockName}.${field.name}`) + if (startWorkflowValue === 'chat') { + // For chat mode, provide input and conversationId + blockTags = [`${normalizedBlockName}.input`, `${normalizedBlockName}.conversationId`] } else { - // Fallback to just the block name - blockTags = [normalizedBlockName] + // Check for custom input format fields (for manual mode) + const inputFormatValue = useSubBlockStore + .getState() + .getValue(activeSourceBlockId, 'inputFormat') + + if ( + inputFormatValue && + Array.isArray(inputFormatValue) && + inputFormatValue.length > 0 + ) { + // Use custom input fields if they exist + blockTags = inputFormatValue + .filter((field: any) => field.name && field.name.trim() !== '') + .map((field: any) => `${normalizedBlockName}.${field.name}`) + } else { + // Fallback to just the block name + blockTags = [normalizedBlockName] + } } } else { // Other blocks with no outputs - show as just @@ -429,19 +443,33 @@ export const TagDropdown: React.FC = ({ } else if (Object.keys(blockConfig.outputs).length === 0) { // Handle blocks with no outputs (like starter) - check for custom input fields if (accessibleBlock.type === 'starter') { - // Check for custom input format fields - const inputFormatValue = useSubBlockStore + // Check what start workflow mode is selected + const startWorkflowValue = useSubBlockStore .getState() - .getValue(accessibleBlockId, 'inputFormat') + .getValue(accessibleBlockId, 'startWorkflow') - if (inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0) { - // Use custom input fields if they exist - blockTags = inputFormatValue - .filter((field: any) => field.name && field.name.trim() !== '') - .map((field: any) => `${normalizedBlockName}.${field.name}`) + if (startWorkflowValue === 'chat') { + // For chat mode, provide input and conversationId + blockTags = [`${normalizedBlockName}.input`, `${normalizedBlockName}.conversationId`] } else { - // Fallback to just the block name - blockTags = [normalizedBlockName] + // Check for custom input format fields (for manual mode) + const inputFormatValue = useSubBlockStore + .getState() + .getValue(accessibleBlockId, 'inputFormat') + + if ( + inputFormatValue && + Array.isArray(inputFormatValue) && + inputFormatValue.length > 0 + ) { + // Use custom input fields if they exist + blockTags = inputFormatValue + .filter((field: any) => field.name && field.name.trim() !== '') + .map((field: any) => `${normalizedBlockName}.${field.name}`) + } else { + // Fallback to just the block name + blockTags = [normalizedBlockName] + } } } else { // Other blocks with no outputs - show as just diff --git a/apps/sim/contexts/socket-context.tsx b/apps/sim/contexts/socket-context.tsx index 90f28477c2..f0f58dc546 100644 --- a/apps/sim/contexts/socket-context.tsx +++ b/apps/sim/contexts/socket-context.tsx @@ -357,7 +357,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { }) socketInstance.on('workflow-state', (state) => { - logger.info('Received workflow state from server:', state) + // logger.info('Received workflow state from server:', state) // This will be used to sync initial state when joining a workflow }) diff --git a/apps/sim/db/migrations/0057_charming_star_brand.sql b/apps/sim/db/migrations/0057_charming_star_brand.sql new file mode 100644 index 0000000000..4825c79305 --- /dev/null +++ b/apps/sim/db/migrations/0057_charming_star_brand.sql @@ -0,0 +1,6 @@ +ALTER TABLE "workflow_schedule" DROP CONSTRAINT "workflow_schedule_workflow_id_unique";--> statement-breakpoint +ALTER TABLE "webhook" ADD COLUMN "block_id" text;--> statement-breakpoint +ALTER TABLE "workflow_schedule" ADD COLUMN "block_id" text;--> statement-breakpoint +ALTER TABLE "webhook" ADD CONSTRAINT "webhook_block_id_workflow_blocks_id_fk" FOREIGN KEY ("block_id") REFERENCES "public"."workflow_blocks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow_schedule" ADD CONSTRAINT "workflow_schedule_block_id_workflow_blocks_id_fk" FOREIGN KEY ("block_id") REFERENCES "public"."workflow_blocks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "workflow_schedule_workflow_block_unique" ON "workflow_schedule" USING btree ("workflow_id","block_id"); \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/0057_snapshot.json b/apps/sim/db/migrations/meta/0057_snapshot.json new file mode 100644 index 0000000000..d9a6cebabe --- /dev/null +++ b/apps/sim/db/migrations/meta/0057_snapshot.json @@ -0,0 +1,5655 @@ +{ + "id": "629121b5-cdc9-4e2f-b671-7e3b11af7af1", + "prevId": "3bb59215-ddd2-4c4a-82e7-4fc9e998ca08", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subdomain_idx": { + "name": "subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 100, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_pan": { + "name": "auto_pan", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "console_expanded_by_default": { + "name": "console_expanded_by_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_notified_user": { + "name": "telemetry_notified_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "general": { + "name": "general", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR (metadata IS NOT NULL AND (metadata->>'perSeatAllowance' IS NOT NULL OR metadata->>'totalAllowance' IS NOT NULL))" + } + }, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'FileText'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_workflow_id_idx": { + "name": "templates_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_id_idx": { + "name": "templates_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_idx": { + "name": "templates_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_views_idx": { + "name": "templates_category_views_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_stars_idx": { + "name": "templates_category_stars_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_category_idx": { + "name": "templates_user_category_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "templates_user_id_user_id_fk": { + "name": "templates_user_id_user_id_fk", + "tableFrom": "templates", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_rate_limits": { + "name": "user_rate_limits", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sync_api_requests": { + "name": "sync_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "async_api_requests": { + "name": "async_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_rate_limited": { + "name": "is_rate_limited", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rate_limit_reset_at": { + "name": "rate_limit_reset_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_rate_limits_user_id_user_id_fk": { + "name": "user_rate_limits_user_id_user_id_fk", + "tableFrom": "user_rate_limits", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'5'" + }, + "usage_limit_set_by": { + "name": "usage_limit_set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extent": { + "name": "extent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_parent_id_idx": { + "name": "workflow_blocks_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_parent_idx": { + "name": "workflow_blocks_workflow_parent_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_type_idx": { + "name": "workflow_blocks_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_source_block_idx": { + "name": "workflow_edges_source_block_idx", + "columns": [ + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_target_block_idx": { + "name": "workflow_edges_target_block_idx", + "columns": [ + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_blocks": { + "name": "workflow_execution_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_name": { + "name": "block_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_type": { + "name": "block_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_stack_trace": { + "name": "error_stack_trace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_data": { + "name": "input_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output_data": { + "name": "output_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_input": { + "name": "cost_input", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "cost_output": { + "name": "cost_output", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "tokens_prompt": { + "name": "tokens_prompt", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_completion": { + "name": "tokens_completion", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_total": { + "name": "tokens_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_blocks_execution_id_idx": { + "name": "execution_blocks_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_workflow_id_idx": { + "name": "execution_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_block_id_idx": { + "name": "execution_blocks_block_id_idx", + "columns": [ + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_status_idx": { + "name": "execution_blocks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_duration_idx": { + "name": "execution_blocks_duration_idx", + "columns": [ + { + "expression": "duration_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_cost_idx": { + "name": "execution_blocks_cost_idx", + "columns": [ + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_workflow_execution_idx": { + "name": "execution_blocks_workflow_execution_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_execution_status_idx": { + "name": "execution_blocks_execution_status_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_blocks_started_at_idx": { + "name": "execution_blocks_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_execution_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "block_count": { + "name": "block_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "success_count": { + "name": "success_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_input_cost": { + "name": "total_input_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_output_cost": { + "name": "total_output_cost", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_idx": { + "name": "workflow_execution_logs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_cost_idx": { + "name": "workflow_execution_logs_cost_idx", + "columns": [ + { + "expression": "total_cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_duration_idx": { + "name": "workflow_execution_logs_duration_idx", + "columns": [ + { + "expression": "total_duration_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_logs": { + "name": "workflow_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_logs_workflow_id_workflow_id_fk": { + "name": "workflow_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index 50ef4abc63..d7979a916f 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -393,6 +393,13 @@ "when": 1752789061522, "tag": "0056_adorable_franklin_richards", "breakpoints": true + }, + { + "idx": 57, + "version": "7", + "when": 1752980338632, + "tag": "0057_charming_star_brand", + "breakpoints": true } ] } diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index 5cc468a472..46e28de8cc 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -410,23 +410,34 @@ export const settings = pgTable('settings', { updatedAt: timestamp('updated_at').notNull().defaultNow(), }) -export const workflowSchedule = pgTable('workflow_schedule', { - id: text('id').primaryKey(), - workflowId: text('workflow_id') - .notNull() - .references(() => workflow.id, { onDelete: 'cascade' }) - .unique(), - cronExpression: text('cron_expression'), - nextRunAt: timestamp('next_run_at'), - lastRanAt: timestamp('last_ran_at'), - triggerType: text('trigger_type').notNull(), // "manual", "webhook", "schedule" - timezone: text('timezone').notNull().default('UTC'), - failedCount: integer('failed_count').notNull().default(0), // Track consecutive failures - status: text('status').notNull().default('active'), // 'active' or 'disabled' - lastFailedAt: timestamp('last_failed_at'), // When the schedule last failed - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), -}) +export const workflowSchedule = pgTable( + 'workflow_schedule', + { + id: text('id').primaryKey(), + workflowId: text('workflow_id') + .notNull() + .references(() => workflow.id, { onDelete: 'cascade' }), + blockId: text('block_id').references(() => workflowBlocks.id, { onDelete: 'cascade' }), + cronExpression: text('cron_expression'), + nextRunAt: timestamp('next_run_at'), + lastRanAt: timestamp('last_ran_at'), + triggerType: text('trigger_type').notNull(), // "manual", "webhook", "schedule" + timezone: text('timezone').notNull().default('UTC'), + failedCount: integer('failed_count').notNull().default(0), // Track consecutive failures + status: text('status').notNull().default('active'), // 'active' or 'disabled' + lastFailedAt: timestamp('last_failed_at'), // When the schedule last failed + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => { + return { + workflowBlockUnique: uniqueIndex('workflow_schedule_workflow_block_unique').on( + table.workflowId, + table.blockId + ), + } + } +) export const webhook = pgTable( 'webhook', @@ -435,6 +446,7 @@ export const webhook = pgTable( workflowId: text('workflow_id') .notNull() .references(() => workflow.id, { onDelete: 'cascade' }), + blockId: text('block_id').references(() => workflowBlocks.id, { onDelete: 'cascade' }), // ID of the webhook trigger block (nullable for legacy starter block webhooks) path: text('path').notNull(), provider: text('provider'), // e.g., "whatsapp", "github", etc. providerConfig: json('provider_config'), // Store provider-specific configuration diff --git a/apps/sim/executor/consts.ts b/apps/sim/executor/consts.ts index b5ebea7154..6304ee848a 100644 --- a/apps/sim/executor/consts.ts +++ b/apps/sim/executor/consts.ts @@ -14,6 +14,8 @@ export enum BlockType { RESPONSE = 'response', WORKFLOW = 'workflow', STARTER = 'starter', + SCHEDULE = 'schedule', + WEBHOOK_TRIGGER = 'webhook_trigger', } /** diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index 22508b7697..0703ff3c87 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -167,9 +167,13 @@ export class Executor { * Executes the workflow and returns the result. * * @param workflowId - Unique identifier for the workflow execution + * @param startBlockId - Optional block ID to start execution from (for webhook or schedule triggers) * @returns Execution result containing output, logs, and metadata, or a stream, or combined execution and stream */ - async execute(workflowId: string): Promise { + async execute( + workflowId: string, + startBlockId?: string + ): Promise { const { setIsExecuting, setIsDebugging, setPendingBlocks, reset } = useExecutionStore.getState() const startTime = new Date() let finalOutput: NormalizedBlockOutput = {} @@ -182,9 +186,9 @@ export class Executor { startTime: startTime.toISOString(), }) - this.validateWorkflow() + this.validateWorkflow(startBlockId) - const context = this.createExecutionContext(workflowId, startTime) + const context = this.createExecutionContext(workflowId, startTime, startBlockId) try { setIsExecuting(true) @@ -543,30 +547,46 @@ export class Executor { /** * Validates that the workflow meets requirements for execution. - * Checks for starter block, connections, and loop configurations. + * Checks for starter block, webhook trigger block, or schedule trigger block, connections, and loop configurations. * + * @param startBlockId - Optional specific block to start from * @throws Error if workflow validation fails */ - private validateWorkflow(): void { - const starterBlock = this.actualWorkflow.blocks.find( - (block) => block.metadata?.id === BlockType.STARTER - ) - if (!starterBlock || !starterBlock.enabled) { - throw new Error('Workflow must have an enabled starter block') - } + private validateWorkflow(startBlockId?: string): void { + let validationBlock: SerializedBlock | undefined - const incomingToStarter = this.actualWorkflow.connections.filter( - (conn) => conn.target === starterBlock.id - ) - if (incomingToStarter.length > 0) { - throw new Error('Starter block cannot have incoming connections') - } + if (startBlockId) { + // If starting from a specific block (webhook trigger or schedule trigger), validate that block exists + const startBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId) + if (!startBlock || !startBlock.enabled) { + throw new Error(`Start block ${startBlockId} not found or disabled`) + } + validationBlock = startBlock + // Trigger blocks (webhook and schedule) can have incoming connections, so no need to check that + } else { + // Default validation for starter block + const starterBlock = this.actualWorkflow.blocks.find( + (block) => block.metadata?.id === BlockType.STARTER + ) + if (!starterBlock || !starterBlock.enabled) { + throw new Error('Workflow must have an enabled starter block') + } + validationBlock = starterBlock - const outgoingFromStarter = this.actualWorkflow.connections.filter( - (conn) => conn.source === starterBlock.id - ) - if (outgoingFromStarter.length === 0) { - throw new Error('Starter block must have at least one outgoing connection') + const incomingToStarter = this.actualWorkflow.connections.filter( + (conn) => conn.target === starterBlock.id + ) + if (incomingToStarter.length > 0) { + throw new Error('Starter block cannot have incoming connections') + } + + // Only check outgoing connections for starter blocks, not trigger blocks + const outgoingFromStarter = this.actualWorkflow.connections.filter( + (conn) => conn.source === starterBlock.id + ) + if (outgoingFromStarter.length === 0) { + throw new Error('Starter block must have at least one outgoing connection') + } } const blockIds = new Set(this.actualWorkflow.blocks.map((block) => block.id)) @@ -603,13 +623,18 @@ export class Executor { /** * Creates the initial execution context with predefined states. - * Sets up the starter block and its connections in the active execution path. + * Sets up the starter block, webhook trigger block, or schedule trigger block and its connections in the active execution path. * * @param workflowId - Unique identifier for the workflow execution * @param startTime - Execution start time + * @param startBlockId - Optional specific block to start from * @returns Initialized execution context */ - private createExecutionContext(workflowId: string, startTime: Date): ExecutionContext { + private createExecutionContext( + workflowId: string, + startTime: Date, + startBlockId?: string + ): ExecutionContext { const context: ExecutionContext = { workflowId, blockStates: new Map(), @@ -652,13 +677,22 @@ export class Executor { } } - const starterBlock = this.actualWorkflow.blocks.find( - (block) => block.metadata?.id === BlockType.STARTER - ) - if (starterBlock) { - // Initialize the starter block with the workflow input + // Determine which block to initialize as the starting point + let initBlock: SerializedBlock | undefined + if (startBlockId) { + // Starting from a specific block (webhook trigger or schedule trigger) + initBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId) + } else { + // Default to starter block + initBlock = this.actualWorkflow.blocks.find( + (block) => block.metadata?.id === BlockType.STARTER + ) + } + + if (initBlock) { + // Initialize the starting block with the workflow input try { - const blockParams = starterBlock.config.params + const blockParams = initBlock.config.params const inputFormat = blockParams?.inputFormat // If input format is defined, structure the input according to the schema @@ -718,17 +752,17 @@ export class Executor { // Use the structured input if we processed fields, otherwise use raw input const finalInput = hasProcessedFields ? structuredInput : rawInputData - // Initialize the starter block with structured input (flattened) - const starterOutput = { + // Initialize the starting block with structured input (flattened) + const blockOutput = { input: finalInput, conversationId: this.workflowInput?.conversationId, // Add conversationId to root ...finalInput, // Add input fields directly at top level } - logger.info(`[Executor] Starter output:`, JSON.stringify(starterOutput, null, 2)) + logger.info(`[Executor] Starting block output:`, JSON.stringify(blockOutput, null, 2)) - context.blockStates.set(starterBlock.id, { - output: starterOutput, + context.blockStates.set(initBlock.id, { + output: blockOutput, executed: true, executionTime: 0, }) @@ -746,7 +780,7 @@ export class Executor { conversationId: this.workflowInput.conversationId, } - context.blockStates.set(starterBlock.id, { + context.blockStates.set(initBlock.id, { output: starterOutput, executed: true, executionTime: 0, @@ -755,7 +789,7 @@ export class Executor { // API workflow: spread the raw data directly (no wrapping) const starterOutput = { ...this.workflowInput } - context.blockStates.set(starterBlock.id, { + context.blockStates.set(initBlock.id, { output: starterOutput, executed: true, executionTime: 0, @@ -767,7 +801,7 @@ export class Executor { input: this.workflowInput, } - context.blockStates.set(starterBlock.id, { + context.blockStates.set(initBlock.id, { output: starterOutput, executed: true, executionTime: 0, @@ -778,7 +812,7 @@ export class Executor { logger.warn('Error processing starter block input format:', e) // Error handler fallback - use appropriate structure - let starterOutput: any + let blockOutput: any if (this.workflowInput && typeof this.workflowInput === 'object') { // Check if this is a chat workflow input (has both input and conversationId) if ( @@ -786,40 +820,43 @@ export class Executor { Object.hasOwn(this.workflowInput, 'conversationId') ) { // Chat workflow: extract input and conversationId to root level - starterOutput = { + blockOutput = { input: this.workflowInput.input, conversationId: this.workflowInput.conversationId, } } else { // API workflow: spread the raw data directly (no wrapping) - starterOutput = { ...this.workflowInput } + blockOutput = { ...this.workflowInput } } } else { // Primitive input - starterOutput = { + blockOutput = { input: this.workflowInput, } } - logger.info('[Executor] Fallback starter output:', JSON.stringify(starterOutput, null, 2)) + logger.info( + '[Executor] Fallback starting block output:', + JSON.stringify(blockOutput, null, 2) + ) - context.blockStates.set(starterBlock.id, { - output: starterOutput, + context.blockStates.set(initBlock.id, { + output: blockOutput, executed: true, executionTime: 0, }) } - // Ensure the starter block is in the active execution path - context.activeExecutionPath.add(starterBlock.id) - // Mark the starter block as executed - context.executedBlocks.add(starterBlock.id) + // 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) - // Add all blocks connected to the starter to the active execution path - const connectedToStarter = this.actualWorkflow.connections - .filter((conn) => conn.source === starterBlock.id) + // 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) - connectedToStarter.forEach((blockId) => { + connectedToStartBlock.forEach((blockId) => { context.activeExecutionPath.add(blockId) }) } diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index b303109ec8..cd7eb6f374 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -289,7 +289,6 @@ export function useCollaborativeWorkflow() { isDeployed: workflowData.state.isDeployed || false, deployedAt: workflowData.state.deployedAt, lastSaved: workflowData.state.lastSaved || Date.now(), - hasActiveSchedule: workflowData.state.hasActiveSchedule || false, hasActiveWebhook: workflowData.state.hasActiveWebhook || false, deploymentStatuses: workflowData.state.deploymentStatuses || {}, }) diff --git a/apps/sim/lib/webhooks/utils.ts b/apps/sim/lib/webhooks/utils.ts index 0edb2bf6ac..0aee84245d 100644 --- a/apps/sim/lib/webhooks/utils.ts +++ b/apps/sim/lib/webhooks/utils.ts @@ -423,7 +423,8 @@ export async function executeWorkflowFromPayload( foundWorkflow: any, input: any, executionId: string, - requestId: string + requestId: string, + startBlockId?: string | null ): Promise { // Add log at the beginning of this function for clarity logger.info(`[${requestId}] Preparing to execute workflow`, { @@ -668,7 +669,7 @@ export async function executeWorkflowFromPayload( ) // This is THE critical line where the workflow actually executes - const result = await executor.execute(foundWorkflow.id) + const result = await executor.execute(foundWorkflow.id, startBlockId || undefined) // Check if we got a StreamingExecution result (with stream + execution properties) // For webhook executions, we only care about the ExecutionResult part, not the stream @@ -1275,7 +1276,7 @@ export async function fetchAndProcessAirtablePayloads( } ) - await executeWorkflowFromPayload(workflowData, input, requestId, requestId) + await executeWorkflowFromPayload(workflowData, input, requestId, requestId, null) // COMPLETION LOG - This will only appear if execution succeeds logger.info(`[${requestId}] CRITICAL_TRACE: Workflow execution completed successfully`, { @@ -1372,7 +1373,13 @@ export async function processWebhook( `[${requestId}] Executing workflow ${foundWorkflow.id} for webhook ${foundWebhook.id} (Execution: ${executionId})` ) - await executeWorkflowFromPayload(foundWorkflow, input, executionId, requestId) + await executeWorkflowFromPayload( + foundWorkflow, + input, + executionId, + requestId, + foundWebhook.blockId + ) // Since executeWorkflowFromPayload handles logging and errors internally, // we just need to return a standard success response for synchronous webhooks. diff --git a/apps/sim/lib/workflows/db-helpers.test.ts b/apps/sim/lib/workflows/db-helpers.test.ts index 66035a06b7..c82858a1b4 100644 --- a/apps/sim/lib/workflows/db-helpers.test.ts +++ b/apps/sim/lib/workflows/db-helpers.test.ts @@ -207,7 +207,6 @@ const mockWorkflowState: WorkflowState = { lastSaved: Date.now(), isDeployed: false, deploymentStatuses: {}, - hasActiveSchedule: false, hasActiveWebhook: false, } @@ -463,7 +462,6 @@ describe('Database Helpers', () => { lastSaved: Date.now(), isDeployed: false, deploymentStatuses: {}, - hasActiveSchedule: false, hasActiveWebhook: false, } @@ -643,7 +641,6 @@ describe('Database Helpers', () => { lastSaved: Date.now(), isDeployed: false, deploymentStatuses: {}, - hasActiveSchedule: false, hasActiveWebhook: false, } @@ -731,7 +728,6 @@ describe('Database Helpers', () => { lastSaved: Date.now(), isDeployed: false, deploymentStatuses: {}, - hasActiveSchedule: false, hasActiveWebhook: false, } diff --git a/apps/sim/lib/workflows/db-helpers.ts b/apps/sim/lib/workflows/db-helpers.ts index 89672ef22e..3545c4c1c2 100644 --- a/apps/sim/lib/workflows/db-helpers.ts +++ b/apps/sim/lib/workflows/db-helpers.ts @@ -211,7 +211,6 @@ export async function saveWorkflowToNormalizedTables( isDeployed: state.isDeployed, deployedAt: state.deployedAt, deploymentStatuses: state.deploymentStatuses, - hasActiveSchedule: state.hasActiveSchedule, hasActiveWebhook: state.hasActiveWebhook, } @@ -264,7 +263,6 @@ export async function migrateWorkflowToNormalizedTables( isDeployed: jsonState.isDeployed, deployedAt: jsonState.deployedAt, deploymentStatuses: jsonState.deploymentStatuses || {}, - hasActiveSchedule: jsonState.hasActiveSchedule, hasActiveWebhook: jsonState.hasActiveWebhook, } diff --git a/apps/sim/socket-server/database/operations.ts b/apps/sim/socket-server/database/operations.ts index 7c1c296ee1..434e6829c7 100644 --- a/apps/sim/socket-server/database/operations.ts +++ b/apps/sim/socket-server/database/operations.ts @@ -130,7 +130,6 @@ export async function getWorkflowState(workflowId: string) { const finalState = { // Default values for expected properties deploymentStatuses: {}, - hasActiveSchedule: false, hasActiveWebhook: false, // Preserve any existing state properties ...existingState, diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index d077538e6f..c49741f7ce 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -188,7 +188,6 @@ function resetWorkflowStores() { isDeployed: false, deployedAt: undefined, deploymentStatuses: {}, // Reset deployment statuses map - hasActiveSchedule: false, history: { past: [], present: { @@ -441,7 +440,6 @@ export const useWorkflowRegistry = create()( lastSaved: Date.now(), marketplaceData: workflowData.marketplaceData || null, deploymentStatuses: {}, - hasActiveSchedule: false, history: { past: [], present: { @@ -490,7 +488,6 @@ export const useWorkflowRegistry = create()( isDeployed: false, deployedAt: undefined, deploymentStatuses: {}, - hasActiveSchedule: false, history: { past: [], present: { @@ -1253,7 +1250,6 @@ export const useWorkflowRegistry = create()( parallels: {}, isDeployed: false, deployedAt: undefined, - hasActiveSchedule: false, history: { past: [], present: { diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index b1308552de..3f624f0032 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -23,7 +23,6 @@ const initialState = { // New field for per-workflow deployment tracking deploymentStatuses: {}, needsRedeployment: false, - hasActiveSchedule: false, hasActiveWebhook: false, history: { past: [], @@ -436,7 +435,6 @@ export const useWorkflowStore = create()( lastSaved: Date.now(), isDeployed: false, isPublished: false, - hasActiveSchedule: false, hasActiveWebhook: false, } set(newState) @@ -799,23 +797,9 @@ export const useWorkflowStore = create()( })) }, - setScheduleStatus: (hasActiveSchedule: boolean) => { - // Only update if the status has changed to avoid unnecessary rerenders - if (get().hasActiveSchedule !== hasActiveSchedule) { - set({ hasActiveSchedule }) - get().updateLastSaved() - // Note: Socket.IO handles real-time sync automatically - } - }, - setWebhookStatus: (hasActiveWebhook: boolean) => { // Only update if the status has changed to avoid unnecessary rerenders if (get().hasActiveWebhook !== hasActiveWebhook) { - // If the workflow has an active schedule, disable it - if (get().hasActiveSchedule) { - get().setScheduleStatus(false) - } - set({ hasActiveWebhook }) get().updateLastSaved() // Note: Socket.IO handles real-time sync automatically diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index 65ef362451..05b31bfbc2 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -140,7 +140,6 @@ export interface WorkflowState { // New field for per-workflow deployment status deploymentStatuses?: Record needsRedeployment?: boolean - hasActiveSchedule?: boolean hasActiveWebhook?: boolean } @@ -189,7 +188,6 @@ export interface WorkflowActions { generateLoopBlocks: () => Record generateParallelBlocks: () => Record setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void - setScheduleStatus: (hasActiveSchedule: boolean) => void setWebhookStatus: (hasActiveWebhook: boolean) => void revertToDeployedState: (deployedState: WorkflowState) => void toggleBlockAdvancedMode: (id: string) => void diff --git a/apps/sim/stores/workflows/yaml/importer.ts b/apps/sim/stores/workflows/yaml/importer.ts index 6ef4d3b84e..d1187d7a94 100644 --- a/apps/sim/stores/workflows/yaml/importer.ts +++ b/apps/sim/stores/workflows/yaml/importer.ts @@ -696,7 +696,6 @@ export async function importWorkflowFromYaml( isDeployed: false, deployedAt: undefined, deploymentStatuses: {}, - hasActiveSchedule: false, hasActiveWebhook: false, }