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

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

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

Subscription will be cancelled at end of billing period

-
-
-
+ + {isLoading ? 'Restoring...' : 'Restore Subscription'} + ) } return ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx index 9ac78581ef..b69b499aff 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx @@ -523,6 +523,7 @@ export function Subscription({ onOpenChange }: SubscriptionProps) { }} subscriptionData={{ periodEnd: subscriptionData?.periodEnd || null, + cancelAtPeriodEnd: subscriptionData?.cancelAtPeriodEnd, }} />
diff --git a/apps/sim/lib/billing/core/billing.ts b/apps/sim/lib/billing/core/billing.ts index 48085eb821..55b6a207f7 100644 --- a/apps/sim/lib/billing/core/billing.ts +++ b/apps/sim/lib/billing/core/billing.ts @@ -220,6 +220,7 @@ export async function getSimplifiedBillingSummary( metadata: any stripeSubscriptionId: string | null periodEnd: Date | string | null + cancelAtPeriodEnd?: boolean // Usage details usage: { current: number @@ -318,6 +319,7 @@ export async function getSimplifiedBillingSummary( metadata: subscription.metadata || null, stripeSubscriptionId: subscription.stripeSubscriptionId || null, periodEnd: subscription.periodEnd || null, + cancelAtPeriodEnd: subscription.cancelAtPeriodEnd || undefined, // Usage details usage: { current: usageData.currentUsage, @@ -393,6 +395,7 @@ export async function getSimplifiedBillingSummary( metadata: subscription?.metadata || null, stripeSubscriptionId: subscription?.stripeSubscriptionId || null, periodEnd: subscription?.periodEnd || null, + cancelAtPeriodEnd: subscription?.cancelAtPeriodEnd || undefined, // Usage details usage: { current: currentUsage, @@ -450,5 +453,14 @@ function getDefaultBillingSummary(type: 'individual' | 'organization') { lastPeriodCost: 0, daysRemaining: 0, }, + ...(type === 'organization' && { + organizationData: { + seatCount: 0, + memberCount: 0, + totalBasePrice: 0, + totalCurrentUsage: 0, + totalOverage: 0, + }, + }), } } diff --git a/apps/sim/stores/subscription/types.ts b/apps/sim/stores/subscription/types.ts index c0de147d45..643694b795 100644 --- a/apps/sim/stores/subscription/types.ts +++ b/apps/sim/stores/subscription/types.ts @@ -29,6 +29,7 @@ export interface SubscriptionData { metadata: any | null stripeSubscriptionId: string | null periodEnd: Date | null + cancelAtPeriodEnd?: boolean usage: UsageData billingBlocked?: boolean } From f6e789186bed5e9129624d4257717937766eef6a Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 5 Nov 2025 10:18:49 -0800 Subject: [PATCH 02/37] Add pause resume block --- apps/sim/blocks/blocks/pause_resume.ts | 123 ++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/executor/consts.ts | 1 + apps/sim/executor/handlers/index.ts | 2 + .../pause-resume/pause-resume-handler.ts | 267 ++++++++++++++++++ apps/sim/executor/handlers/registry.ts | 2 + 6 files changed, 397 insertions(+) create mode 100644 apps/sim/blocks/blocks/pause_resume.ts create mode 100644 apps/sim/executor/handlers/pause-resume/pause-resume-handler.ts diff --git a/apps/sim/blocks/blocks/pause_resume.ts b/apps/sim/blocks/blocks/pause_resume.ts new file mode 100644 index 0000000000..82bcb07ea4 --- /dev/null +++ b/apps/sim/blocks/blocks/pause_resume.ts @@ -0,0 +1,123 @@ +import { ResponseIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import type { ResponseBlockOutput } from '@/tools/response/types' + +export const PauseResumeBlock: BlockConfig = { + type: 'pause_resume', + name: 'Pause Resume', + description: 'Pause workflow execution and send structured API response', + longDescription: + 'Combines response and start functionality. Sends structured responses and allows workflow to resume from this point.', + category: 'blocks', + bgColor: '#2F55FF', + icon: ResponseIcon, + subBlocks: [ + { + id: 'dataMode', + title: 'Response Data Mode', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Builder', id: 'structured' }, + { label: 'Editor', id: 'json' }, + ], + value: () => 'structured', + description: 'Choose how to define your response data structure', + }, + { + id: 'builderData', + title: 'Response Structure', + type: 'response-format', + layout: 'full', + condition: { field: 'dataMode', value: 'structured' }, + description: + 'Define the structure of your response data. Use in field names to reference workflow variables.', + }, + { + id: 'data', + title: 'Response Data', + type: 'code', + layout: 'full', + placeholder: '{\n "message": "Hello world",\n "userId": ""\n}', + language: 'json', + condition: { field: 'dataMode', value: 'json' }, + description: + 'Data that will be sent as the response body on API calls. Use to reference workflow variables.', + wandConfig: { + enabled: true, + maintainHistory: true, + prompt: `You are an expert JSON programmer. +Generate ONLY the raw JSON object based on the user's request. +The output MUST be a single, valid JSON object, starting with { and ending with }. + +Current response: {context} + +Do not include any explanations, markdown formatting, or other text outside the JSON object. + +You have access to the following variables you can use to generate the JSON body: +- 'params' (object): Contains input parameters derived from the JSON schema. Access these directly using the parameter name wrapped in angle brackets, e.g., ''. Do NOT use 'params.paramName'. +- 'environmentVariables' (object): Contains environment variables. Reference these using the double curly brace syntax: '{{ENV_VAR_NAME}}'. Do NOT use 'environmentVariables.VAR_NAME' or env. + +Example: +{ + "name": "", + "age": , + "success": true +}`, + placeholder: 'Describe the API response structure you need...', + generationType: 'json-object', + }, + }, + { + id: 'status', + title: 'Status Code', + type: 'short-input', + layout: 'half', + placeholder: '200', + description: 'HTTP status code (default: 200)', + }, + { + id: 'headers', + title: 'Response Headers', + type: 'table', + layout: 'full', + columns: ['Key', 'Value'], + description: 'Additional HTTP headers to include in the response', + }, + { + id: 'inputFormat', + title: 'Input Format', + type: 'input-format', + layout: 'full', + description: 'Add custom fields beyond the built-in input, conversationId, and files fields.', + }, + ], + tools: { access: [] }, + inputs: { + dataMode: { + type: 'string', + description: 'Response data definition mode', + }, + builderData: { + type: 'json', + description: 'Structured response data', + }, + data: { + type: 'json', + description: 'JSON response body', + }, + status: { + type: 'number', + description: 'HTTP status code', + }, + headers: { + type: 'json', + description: 'Response headers', + }, + }, + outputs: { + data: { type: 'json', description: 'Response data' }, + status: { type: 'number', description: 'HTTP status code' }, + headers: { type: 'json', description: 'Response headers' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index a63483718e..a44e3b5428 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -57,6 +57,7 @@ import { QdrantBlock } from '@/blocks/blocks/qdrant' import { RedditBlock } from '@/blocks/blocks/reddit' import { ResendBlock } from '@/blocks/blocks/resend' import { ResponseBlock } from '@/blocks/blocks/response' +import { PauseResumeBlock } from '@/blocks/blocks/pause_resume' import { RouterBlock } from '@/blocks/blocks/router' import { S3Block } from '@/blocks/blocks/s3' import { ScheduleBlock } from '@/blocks/blocks/schedule' @@ -148,6 +149,7 @@ export const registry: Record = { memory: MemoryBlock, reddit: RedditBlock, response: ResponseBlock, + pause_resume: PauseResumeBlock, router: RouterBlock, schedule: ScheduleBlock, s3: S3Block, diff --git a/apps/sim/executor/consts.ts b/apps/sim/executor/consts.ts index ffc8c96d7b..2384569a8c 100644 --- a/apps/sim/executor/consts.ts +++ b/apps/sim/executor/consts.ts @@ -29,6 +29,7 @@ export enum BlockType { // I/O RESPONSE = 'response', + PAUSE_RESUME = 'pause_resume', WORKFLOW = 'workflow', WORKFLOW_INPUT = 'workflow_input', diff --git a/apps/sim/executor/handlers/index.ts b/apps/sim/executor/handlers/index.ts index acd7db896b..4ec1b70608 100644 --- a/apps/sim/executor/handlers/index.ts +++ b/apps/sim/executor/handlers/index.ts @@ -5,6 +5,7 @@ import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-h import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler' import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler' import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler' +import { PauseResumeBlockHandler } from '@/executor/handlers/pause-resume/pause-resume-handler' import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' import { TriggerBlockHandler } from '@/executor/handlers/trigger/trigger-handler' import { VariablesBlockHandler } from '@/executor/handlers/variables/variables-handler' @@ -19,6 +20,7 @@ export { FunctionBlockHandler, GenericBlockHandler, ResponseBlockHandler, + PauseResumeBlockHandler, RouterBlockHandler, TriggerBlockHandler, VariablesBlockHandler, diff --git a/apps/sim/executor/handlers/pause-resume/pause-resume-handler.ts b/apps/sim/executor/handlers/pause-resume/pause-resume-handler.ts new file mode 100644 index 0000000000..94f68f03b3 --- /dev/null +++ b/apps/sim/executor/handlers/pause-resume/pause-resume-handler.ts @@ -0,0 +1,267 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { BlockOutput } from '@/blocks/types' +import { BlockType, HTTP } from '@/executor/consts' +import type { BlockHandler, ExecutionContext } from '@/executor/types' +import type { SerializedBlock } from '@/serializer/types' + +const logger = createLogger('PauseResumeBlockHandler') + +interface JSONProperty { + id: string + name: string + type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files' + value: any + collapsed?: boolean +} + +export class PauseResumeBlockHandler implements BlockHandler { + canHandle(block: SerializedBlock): boolean { + return block.metadata?.id === BlockType.PAUSE_RESUME + } + + async execute( + ctx: ExecutionContext, + block: SerializedBlock, + inputs: Record + ): Promise { + logger.info(`Executing pause resume block: ${block.id}`) + + try { + const responseData = this.parseResponseData(inputs) + const statusCode = this.parseStatus(inputs.status) + const responseHeaders = this.parseHeaders(inputs.headers) + + logger.info('Pause resume prepared', { + status: statusCode, + dataKeys: Object.keys(responseData), + headerKeys: Object.keys(responseHeaders), + }) + + return { + response: { + data: responseData, + status: statusCode, + headers: responseHeaders, + }, + } + } catch (error: any) { + logger.error('Pause resume block execution failed:', error) + return { + response: { + data: { + error: 'Pause resume block execution failed', + message: error.message || 'Unknown error', + }, + status: HTTP.STATUS.SERVER_ERROR, + headers: { 'Content-Type': HTTP.CONTENT_TYPE.JSON }, + }, + } + } + } + + private parseResponseData(inputs: Record): any { + const dataMode = inputs.dataMode || 'structured' + + if (dataMode === 'json' && inputs.data) { + if (typeof inputs.data === 'string') { + try { + return JSON.parse(inputs.data) + } catch (error) { + logger.warn('Failed to parse JSON data, returning as string:', error) + return inputs.data + } + } else if (typeof inputs.data === 'object' && inputs.data !== null) { + return inputs.data + } + return inputs.data + } + + if (dataMode === 'structured' && inputs.builderData) { + const convertedData = this.convertBuilderDataToJson(inputs.builderData) + return this.parseObjectStrings(convertedData) + } + + return inputs.data || {} + } + + private convertBuilderDataToJson(builderData: JSONProperty[]): any { + if (!Array.isArray(builderData)) { + return {} + } + + const result: any = {} + + for (const prop of builderData) { + if (!prop.name || !prop.name.trim()) { + continue + } + + const value = this.convertPropertyValue(prop) + result[prop.name] = value + } + + return result + } + + static convertBuilderDataToJsonString(builderData: JSONProperty[]): string { + if (!Array.isArray(builderData) || builderData.length === 0) { + return '{\n \n}' + } + + const result: any = {} + + for (const prop of builderData) { + if (!prop.name || !prop.name.trim()) { + continue + } + + result[prop.name] = prop.value + } + + let jsonString = JSON.stringify(result, null, 2) + + jsonString = jsonString.replace(/"(<[^>]+>)"/g, '$1') + + return jsonString + } + + private convertPropertyValue(prop: JSONProperty): any { + switch (prop.type) { + case 'object': + return this.convertObjectValue(prop.value) + case 'array': + return this.convertArrayValue(prop.value) + case 'number': + return this.convertNumberValue(prop.value) + case 'boolean': + return this.convertBooleanValue(prop.value) + case 'files': + return prop.value + default: + return prop.value + } + } + + private convertObjectValue(value: any): any { + if (Array.isArray(value)) { + return this.convertBuilderDataToJson(value) + } + + if (typeof value === 'string' && !this.isVariableReference(value)) { + return this.tryParseJson(value, value) + } + + return value + } + + private convertArrayValue(value: any): any { + if (Array.isArray(value)) { + return value.map((item: any) => this.convertArrayItem(item)) + } + + if (typeof value === 'string' && !this.isVariableReference(value)) { + const parsed = this.tryParseJson(value, value) + return Array.isArray(parsed) ? parsed : value + } + + return value + } + + private convertArrayItem(item: any): any { + if (typeof item !== 'object' || !item.type) { + return item + } + + if (item.type === 'object' && Array.isArray(item.value)) { + return this.convertBuilderDataToJson(item.value) + } + + if (item.type === 'array' && Array.isArray(item.value)) { + return item.value.map((subItem: any) => + typeof subItem === 'object' && subItem.type ? subItem.value : subItem + ) + } + + return item.value + } + + private convertNumberValue(value: any): any { + if (this.isVariableReference(value)) { + return value + } + + const numValue = Number(value) + return Number.isNaN(numValue) ? value : numValue + } + + private convertBooleanValue(value: any): any { + if (this.isVariableReference(value)) { + return value + } + + return value === 'true' || value === true + } + + private tryParseJson(jsonString: string, fallback: any): any { + try { + return JSON.parse(jsonString) + } catch { + return fallback + } + } + + private isVariableReference(value: any): boolean { + return typeof value === 'string' && value.trim().startsWith('<') && value.trim().includes('>') + } + + private parseObjectStrings(data: any): any { + if (typeof data === 'string') { + try { + const parsed = JSON.parse(data) + if (typeof parsed === 'object' && parsed !== null) { + return this.parseObjectStrings(parsed) + } + return parsed + } catch { + return data + } + } else if (Array.isArray(data)) { + return data.map((item) => this.parseObjectStrings(item)) + } else if (typeof data === 'object' && data !== null) { + const result: any = {} + for (const [key, value] of Object.entries(data)) { + result[key] = this.parseObjectStrings(value) + } + return result + } + return data + } + + private parseStatus(status?: string): number { + if (!status) return HTTP.STATUS.OK + const parsed = Number(status) + if (Number.isNaN(parsed) || parsed < 100 || parsed > 599) { + return HTTP.STATUS.OK + } + return parsed + } + + private parseHeaders( + headers: { + id: string + cells: { Key: string; Value: string } + }[] + ): Record { + const defaultHeaders = { 'Content-Type': HTTP.CONTENT_TYPE.JSON } + if (!headers) return defaultHeaders + + const headerObj = headers.reduce((acc: Record, header) => { + if (header?.cells?.Key && header?.cells?.Value) { + acc[header.cells.Key] = header.cells.Value + } + return acc + }, {}) + + return { ...defaultHeaders, ...headerObj } + } +} diff --git a/apps/sim/executor/handlers/registry.ts b/apps/sim/executor/handlers/registry.ts index 52f8776b3e..6db9dfb8fc 100644 --- a/apps/sim/executor/handlers/registry.ts +++ b/apps/sim/executor/handlers/registry.ts @@ -13,6 +13,7 @@ import { EvaluatorBlockHandler } from './evaluator/evaluator-handler' import { FunctionBlockHandler } from './function/function-handler' import { GenericBlockHandler } from './generic/generic-handler' import { ResponseBlockHandler } from './response/response-handler' +import { PauseResumeBlockHandler } from './pause-resume/pause-resume-handler' import { RouterBlockHandler } from './router/router-handler' import { TriggerBlockHandler } from './trigger/trigger-handler' import { VariablesBlockHandler } from './variables/variables-handler' @@ -34,6 +35,7 @@ export function createBlockHandlers(): BlockHandler[] { new ConditionBlockHandler(), new RouterBlockHandler(), new ResponseBlockHandler(), + new PauseResumeBlockHandler(), new AgentBlockHandler(), new VariablesBlockHandler(), new WorkflowBlockHandler(), From b99426893f59666f8b182e70728ba86670c20dd9 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 5 Nov 2025 11:38:40 -0800 Subject: [PATCH 03/37] Add db schema --- DESIGN_REVIEW.md | 384 + PAUSE_RESUME_DESIGN.md | 378 + apps/sim/executor/dag/types.ts | 2 +- .../0106_bitter_captain_midlands.sql | 37 + .../db/migrations/meta/0106_snapshot.json | 7886 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 9 +- packages/db/schema.ts | 52 + 7 files changed, 8746 insertions(+), 2 deletions(-) create mode 100644 DESIGN_REVIEW.md create mode 100644 PAUSE_RESUME_DESIGN.md create mode 100644 packages/db/migrations/0106_bitter_captain_midlands.sql create mode 100644 packages/db/migrations/meta/0106_snapshot.json diff --git a/DESIGN_REVIEW.md b/DESIGN_REVIEW.md new file mode 100644 index 0000000000..fb205740df --- /dev/null +++ b/DESIGN_REVIEW.md @@ -0,0 +1,384 @@ +# Pause-Resume Design Review - Issues & Questions + +## 🔴 Critical Issues + +### 1. **Contradictory Pause Collection Logic** (Sections 3.2, 3.5, 3.7) + +**Problem:** Multiple sections say "entire execution pauses immediately" and "first pause wins", but the refined design says "execute ALL blocks in queue before pausing." + +**Old Logic (Section 3.7):** +```typescript +// Check if execution already paused +if (ctx.metadata.pausedAt) { + logger.info('Execution already paused, skipping this pause') + return { paused: false } // Skip this pause +} +``` + +**New Logic (Should be):** +```typescript +// Collect ALL pauses, don't skip any +return { + response: responseOutput, + _pauseMetadata: { isPause: true, ... } +} + +// ExecutionEngine collects ALL pauses in pausedBlocks Map +// After queue empties, saves ALL to single snapshot +``` + +**Which is correct?** +- If "execute all blocks in queue", then MULTIPLE pauses should ALL be collected +- Section 3.5 shows 3 pauses in parallel distribution - this should work fine! +- NO pause should be skipped + +**Resolution needed:** Remove "first pause wins" logic and clarify that ALL pauses are collected. + +--- + +### 2. **Edge Wiring for pause_resume Block Not Specified** + +**DAG Transformation shows:** +``` +[Block A] -> [pause_resume_response] -> ??? + + ??? -> [pause_resume_trigger] -> [Block B] +``` + +**Questions:** +1. Is there an edge from `_response` to `_trigger`? **Probably NO** (resume happens later) +2. How do we wire the original edges? + - Original: `A -> pause_resume -> B` + - Transformed: `A -> pause_response` (no outgoing edge) + - And separately: `pause_trigger -> B` +3. How does the executor know which blocks to queue after resume? + - Answer: `pendingQueue` in snapshot points to `pause_trigger` + - `pause_trigger` has edges to `B` + - So on resume, `B` gets queued from trigger's outgoing edges + +**Needs clarification in DAG transformation section.** + +--- + +### 3. **Bug in Resume API - contextId Reference** + +**Section 5, executeResumeAsync():** +```typescript +await resumeWorkflowExecution({ + snapshot, + newExecutionId, + resumeInput, + workflowId: pausedExecution.workflowId, + originalExecutionId: pausedExecution.executionId, + contextId: pausedExecution.contextId, // ← BUG! No single contextId field +}) +``` + +**Should be:** +```typescript +contextId: contextId, // From route params, not pausedExecution +``` + +**The pausedExecution row doesn't have a single contextId - it has pause_points JSON!** + +--- + +### 4. **Inconsistent Snapshot Count Description** + +**Architecture Summary says:** +> 5. **Independent Snapshots**: Each pause gets own snapshot + +**But the design changed to:** +> ONE snapshot for entire execution, multiple pause points in JSON + +**Needs update.** + +--- + +### 5. **Missing: How to Handle Pause Inside Resumed Execution** + +**Scenario:** +``` +Initial run: pause at pause1, save snapshot1 +Resume pause1: execute blocks, hit pause2, save snapshot2 +``` + +**Questions:** +1. Does snapshot2 get a NEW execution_id? **YES** (per design: newExecutionId for resume) +2. So we'd have: + - `paused_executions` row with `execution_id='exec_123'` (pause1) + - `paused_executions` row with `execution_id='exec_resumed_1'` (pause2) +3. How does `processQueuedResumes()` work with execution_id from resumed execution? + - It checks `parent_execution_id` in resume_queue + - But parent_execution_id should point to original execution + - What if the resumed execution has a different execution_id? + +**Example:** +``` +exec_123: pauses at pause1 + resume -> exec_resumed_1: pauses at pause2 + resume -> exec_resumed_2: completes + +Resume queue entries: +- parent_execution_id: exec_123, new_execution_id: exec_resumed_1 +- parent_execution_id: exec_123, new_execution_id: exec_resumed_2 ← Should this be exec_resumed_1? +``` + +**Is parent_execution_id always the ORIGINAL execution, or the immediate parent?** + +This affects the resume chain logic! + +--- + +## 🟡 Design Questions + +### 6. **What Happens to Pause _response Block's Outgoing Edges?** + +When transforming: +``` +Original: [A] -> [pause] -> [B] + +After: +[A] -> [pause_response] ← Where do pause's outgoing edges go? + [pause_trigger] -> [B] ← This gets pause's outgoing edges? +``` + +**Assumption:** +- `pause_response` has NO outgoing edges (terminal for that execution) +- `pause_trigger` inherits ALL of pause_resume's original outgoing edges +- On resume, trigger activates and execution continues normally + +**Needs explicit documentation.** + +--- + +### 7. **Parallel Branches Continue After One Pauses?** + +**Current design says:** Execute all blocks in queue before pausing + +**Scenario:** +``` +parallel { + branch0: [pause] (completes at T1, adds to pausedBlocks) + branch1: [long_task] (still in queue, started at T0) +} +``` + +**Timeline:** +- T0: Both blocks start executing concurrently +- T1: pause completes, added to pausedBlocks Map +- T2: long_task still running (promise still in executing Set) +- T3: Queue empty, but executing.size > 0 +- T4: Wait for long_task to finish +- T5: Queue empty AND executing empty → save pause + +**So YES, branches continue! This is correct per design.** + +**But sections 3.2-3.3 suggest execution "pauses immediately" - misleading!** + +**Should clarify:** +- Pause blocks don't stop execution +- Queue continues processing +- Only after queue empty AND all executing promises resolve, then save pause + +--- + +### 8. **Resume Creates New Pause - Parent Execution ID Chain** + +**Scenario:** +``` +exec_original: pause1 + └─ resume(pause1) -> exec_resume_1: pause2 + └─ resume(pause2) -> exec_resume_2: complete +``` + +**Resume queue should track:** +``` +Entry 1: + parent_execution_id: exec_original + new_execution_id: exec_resume_1 + +Entry 2: + parent_execution_id: exec_original ← or exec_resume_1? + new_execution_id: exec_resume_2 +``` + +**If parent_execution_id is ALWAYS original:** +- ✅ All resumes for a workflow chain tracked under one parent +- ✅ `processQueuedResumes()` only checks original execution +- ❌ Doesn't reflect actual resume chain + +**If parent_execution_id is immediate parent:** +- ✅ Reflects true execution chain +- ❌ `processQueuedResumes()` wouldn't find exec_resume_2 when exec_resume_1 finishes +- ❌ Chain would break! + +**Current design assumes parent_execution_id is ALWAYS original.** +**This works for Phase 1 but needs clarification.** + +--- + +### 9. **Pause in Loop - Multiple Snapshots?** + +**Scenario:** +``` +loop (3 iterations) { + [A] -> [pause] -> [B] +} +``` + +**Execution:** +- Iteration 0: pause, save snapshot with pause_loop0 +- Resume: execute B, continue to iteration 1 +- Iteration 1: pause, save snapshot with pause_loop1 +- Resume: execute B, continue to iteration 2 +- Iteration 2: pause, save snapshot with pause_loop2 + +**We'd have 3 separate paused_executions rows:** +- `execution_id='exec_123'` with `pause_points={'pause_loop0': ...}` +- `execution_id='exec_resumed_1'` with `pause_points={'pause_loop1': ...}` +- `execution_id='exec_resumed_2'` with `pause_points={'pause_loop2': ...}` + +**This seems correct - each is a different execution.** + +**But what if the loop pauses multiple times in ONE iteration:** +``` +loop { + parallel { + [pause1] + [pause2] + } +} +``` + +Iteration 0: Both pause, ONE snapshot, execution_id='exec_123' +Resume pause1: continues, iteration 1 +Iteration 1: Both pause again, ONE snapshot, execution_id='exec_resumed_1' + +**This makes sense!** + +--- + +### 10. **Multiple Pauses in Parallel - All Collected or First Wins?** + +**Section 3.2 says:** "Branch 0 hits pause1 first, entire execution pauses immediately" + +**But new design says:** Execute all blocks, collect ALL pauses + +**Real behavior should be:** +``` +parallel { + branch0: [pause1] ← Executes + branch1: [pause2] ← Executes concurrently +} + +Result: BOTH in pausedBlocks Map +Saved: ONE snapshot with pause_points={pause1: ..., pause2: ...} +Can resume: pause1 OR pause2 independently +``` + +**Section 3.2 is OUTDATED and contradicts the refined design!** + +--- + +## 🟢 Clarification Needed + +### 11. **DAG Node Access in PauseResumeBlockHandler** + +**Section 4, Handler code:** +```typescript +const contextId = generatePauseContextId( + block.id.replace('_response', ''), + ctx, + node // ← Where does 'node' come from? +) +``` + +**The handler signature is:** +```typescript +async execute( + ctx: ExecutionContext, + block: SerializedBlock, + inputs: Record +): Promise +``` + +**No `node` parameter!** + +**Need to:** +- Pass node to handler? OR +- Get node metadata from ctx? OR +- Generate contextId differently? + +--- + +### 12. **Resume from Different Pause Points - Execution Paths Diverge** + +**Scenario:** +``` +parallel { + branch0: [A] -> [pause1] -> [B] -> [C] + branch1: [D] -> [pause2] -> [E] -> [F] +} +``` + +**Both pause, save ONE snapshot.** + +**Resume pause1:** +- Executes from pause1_trigger +- B, C, E (branch1 continues from after pause2_response), F +- Completes + +**Resume pause2:** +- Executes from pause2_trigger +- E, F, B (branch0 continues from after pause1_response), C +- Completes + +**WAIT - this seems wrong!** + +If both pauses executed, then both `pause1_response` and `pause2_response` are in executedBlocks. + +On resume(pause1): +- Start from pause1_trigger +- pause2_response already executed +- Does pause2_trigger also get queued? + +**Actually, I think the issue is:** +- When pause blocks execute, they DON'T queue their trigger blocks +- Trigger blocks are ONLY queued during resume (via pendingQueue) +- So on resume(pause1), only pause1_trigger is queued +- pause2_trigger is NOT queued (pause2 is still paused) + +**This makes sense!** But needs clarification. + +--- + +### 13. **Architecture Summary Says "Independent Snapshots"** + +Line 2719: "5. **Independent Snapshots**: Each pause gets own snapshot" + +**Should be:** "Single Snapshot Per Execution: All pauses share one snapshot" + +--- + +## Summary + +### Must Fix: +1. Remove "first pause wins" logic from sections 3.2, 3.5, 3.7 +2. Clarify that ALL pauses are collected when queue empties +3. Fix bug in executeResumeAsync (contextId reference) +4. Fix `node` parameter issue in handler +5. Update Architecture Summary principle #5 +6. Fix execution flow example ("save both snapshots" → "save ONE snapshot") + +### Should Clarify: +1. Edge wiring for pause_resume transformation +2. parent_execution_id is always original (document this explicitly) +3. Trigger blocks only queued during resume, not during initial execution +4. Multiple pauses in parallel all collected, not "first wins" + +### Questions for User: +1. Should parent_execution_id always point to original execution, or immediate parent? +2. How should we get DAGNode in PauseResumeBlockHandler for context ID generation? +3. Are the pause response blocks truly terminal (no outgoing edges), and trigger blocks get all original outgoing edges? + diff --git a/PAUSE_RESUME_DESIGN.md b/PAUSE_RESUME_DESIGN.md new file mode 100644 index 0000000000..835ace0d4e --- /dev/null +++ b/PAUSE_RESUME_DESIGN.md @@ -0,0 +1,378 @@ +# Pause-Resume Architecture (2025 refresh) + +## 1. Overview & Goals + +We are introducing a pause-resume block that lets a workflow: +- return an intermediate response, +- persist its full execution state, and +- resume from the exact pause point when an external caller provides input. + +Primary goals: +- **Zero regression** for existing executions (latency, logging, telemetry). +- **Layered architecture**: the core execution engine remains deterministic and database-agnostic. +- **Single source of truth**: one snapshot + one database row per execution, even with multiple pauses. +- **Deterministic resume chaining**: only one resume runs at a time; additional resumes queue FIFO. + +## 2. Design Principles + +1. **Keep the engine pure** – `ExecutionEngine` continues to manage only in-memory DAG traversal. It never talks to the database or queues. +2. **Orchestrate above the engine** – a new `PauseResumeManager` (invoked from `execution-core.ts`) handles persistence and queued resumes once the engine signals a pause. +3. **One snapshot, many pause points** – each execution stores a single serialized snapshot with a JSON map of pause contexts. +4. **Same pipeline for resume** – resume runs flow through `Executor.execute(...)` / `execution-core.ts`, ensuring logging, telemetry, and run-count updates behave identically to initial executions. +5. **Incremental type changes** – extend handler output and execution metadata just enough to describe pauses; non-pausing workflows remain unaffected. + +## 3. System Components + +### 3.1 Execution Engine (existing) +- Gains the ability to: + - detect `_pauseMetadata` emitted by handlers, + - accumulate pause descriptors in-memory, + - return an `ExecutionResult` with `status: 'paused' | 'completed'` plus `pausePoints` and `snapshotSeed` when pauses are present. +- Stays unaware of persistence, networking, or resume queues. +- Only performs snapshot serialization on demand (when `pausedPoints.length > 0`). + +### 3.2 PauseResumeManager (new, orchestrator layer) +- Lives alongside current orchestration in `apps/sim/lib/workflows/executor/execution-core.ts`. +- Responsibilities: + 1. On initial run completion: + - If `result.status === 'paused'`: serialize snapshot, write to DB, log pause metadata, and enqueue pending resumes if necessary. + - If `result.status === 'completed'`: update run counts and exit (existing behaviour). + 2. On resume API call: + - Load paused execution row, validate context, and enqueue/claim a resume queue entry (using `SELECT FOR UPDATE`). + - If granted immediately, call `executeFromSnapshot(...)` (see §3.3). + 3. After any run finishes (paused or completed): + - Atomically check the resume queue; if entries exist, claim the next one and invoke another execution. + 4. Update pause point JSON (`resumeStatus`, timestamps, metadata) atomically. +- Exposes a narrow interface used both by API routes and the execution pipeline. + +### 3.3 Snapshot Serializer (existing + extensions) +- `ExecutionSnapshot` is extended to include: + - `pauseTriggerIds: string[]` (all `_trigger` blocks created during DAG transform), + - `pendingQueue?: string[]` (set only when resuming), + - complete maps for loop/parallel scopes, routing decisions, variables, etc. +- A helper `SnapshotSerializer` converts between runtime structures (Maps/Sets) and plain JSON. +- Serialization occurs only when pauses are detected, preserving hot-path performance. + +### 3.4 Resume-aware Executor API +- Add `Executor.executeFromSnapshot({ snapshot, pendingBlocks, contextExtensions })` that: + 1. Restores block state/metadata into a fresh `ExecutionContext`. + 2. Calls the existing execution pipeline (`Executor.execute`) with a new execution ID. +- `Executor.continueExecution` is implemented atop this helper and used both for pause/resume and future debugger features. + +### 3.5 PauseResumeBlock Handler updates +- Handler now returns: + ```ts + { + response: { ... }, + _pauseMetadata: { + contextId, + triggerBlockId, + response, + blockId, + timestamp, + } + } + ``` +- `BlockExecutor` passes DAG node metadata (loop/parallel context) into handlers that opt-in, enabling `contextId` generation. +- `_pauseMetadata` is optional; other handlers remain unchanged. + +### 3.6 DAG Transformation +- NodeConstructor creates two virtual nodes per pause block: + - `__response` (type `pause_resume_response`, terminal block executed during initial run). + - `__trigger` (type `pause_resume_trigger`, dormant until resume). +- Outgoing edges from the original pause block are rewired to originate from `_trigger`. +- `_response` has no outgoing edges. +- `_trigger` is excluded from initial queue seeding (`ExecutionEngine.initializeQueue` skips nodes with `metadata.isResumeTrigger`). The manager injects trigger IDs into `pendingQueue` when resuming. +- Parallel/loop metadata is copied so context IDs remain unique (e.g., `pause₍branch₎__response`). + +### 3.7 Type System Extensions +- `NormalizedBlockOutput` gains optional `_pauseMetadata`. +- `ExecutionContext.metadata` includes: + - `status: 'running' | 'paused' | 'completed'`, + - `pausePoints?: string[]`, + - `resumeChain?: { parentExecutionId?: string; depth: number }`. +- `ExecutionResult` adds `status` and optional `pausePoints`, `snapshotSeedId`. +- All additions are optional to avoid impacting existing consumers. + +## 4. Execution Lifecycle + +### 4.1 Initial Run +1. `execution-core.ts` serializes the workflow and creates an `Executor` instance (unchanged). +2. `Executor.execute(workflowId)` runs the DAG; if the pause block is reached, the handler emits `_pauseMetadata`. +3. The engine drains the queue, aggregates pause metadata, constructs a snapshot, and returns `status: 'paused'` with `pausePoints` and `snapshotSeed`. +4. `execution-core.ts` hands control to `PauseResumeManager`: + - Persist snapshot + pause points to `paused_executions` (single row per execution). + - Record `paused` status in execution logs without incrementing run counts. + - Respond to the caller with the pause payload (HTTP response from the block). + +### 4.2 Resume Request +1. Client POSTs `/api/resume/{workflowId}/{executionId}/{contextId}` with optional input. +2. API route delegates to `PauseResumeManager.resume(...)`: + - Locks the paused execution row, + - Verifies the pause point is still `paused`, + - Checks the resume queue for active entries. +3. If another resume is executing, a `pending` entry is inserted and the API responds `{ status: 'queued', queuePosition }`. +4. Otherwise, a `claimed` entry is created and `executeFromSnapshot` is invoked asynchronously (fire-and-forget to keep API latency low). + +### 4.3 Resume Execution Flow +1. Manager loads the snapshot, converts it back to runtime structures, and injects the new execution ID. +2. The trigger block corresponding to `contextId` is pre-marked as executed with the resume input, and its node ID is supplied as the pending queue (`pendingBlocks`). +3. Execution proceeds through the same pipeline and can either reach completion or pause again. +4. On completion: + - Execution logs, telemetry, and run counts update exactly once (for the execution that actually completes the workflow). + - The manager marks the resume queue entry `completed` and updates pause point JSON (resumeStatus -> `resumed`, timestamps, metadata). + - If all pause points are resumed, the row transitions to `fully_resumed` and can be deleted or archived later. +5. On a secondary pause: + - The manager stores a **new** row in `paused_executions` keyed by the new execution ID, preserving the resume chain (`parent_execution_id`). + +## 5. Database Schema + +### 5.1 `paused_executions` +```sql +CREATE TABLE paused_executions ( + id TEXT PRIMARY KEY, + workflow_id TEXT NOT NULL, + execution_id TEXT NOT NULL UNIQUE, + execution_snapshot JSONB NOT NULL, + pause_points JSONB NOT NULL, -- { contextId: { response, triggerBlockId, resumeStatus, ... } } + total_pause_count INTEGER NOT NULL, + resumed_count INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'paused', -- 'paused' | 'partially_resumed' | 'fully_resumed' | 'expired' + metadata JSONB DEFAULT '{}', -- e.g., { "pauseScope": "execution" } + paused_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + expires_at TIMESTAMP +); +``` + +### 5.2 `resume_queue` + ```sql +CREATE TABLE resume_queue ( + id TEXT PRIMARY KEY, + paused_execution_id TEXT NOT NULL REFERENCES paused_executions(id) ON DELETE CASCADE, + parent_execution_id TEXT NOT NULL, -- immediate parent in the resume chain + new_execution_id TEXT NOT NULL, + context_id TEXT NOT NULL, + resume_input JSONB, + status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'claimed' | 'completed' | 'failed' + queued_at TIMESTAMP NOT NULL DEFAULT NOW(), + claimed_at TIMESTAMP, + completed_at TIMESTAMP, + failure_reason TEXT +); + +CREATE INDEX resume_queue_parent_idx ON resume_queue(parent_execution_id, status, queued_at); +CREATE INDEX resume_queue_new_exec_idx ON resume_queue(new_execution_id); +``` + +### 5.3 Execution Logs (`workflow_execution_logs`) +- Gains metadata fields for pause awareness: + ```json + { + "status": "paused", + "pausePoints": ["pause₍0₎", "pause₍1₎"], + "resumeChain": { + "parentExecutionId": "exec_123", + "depth": 1 + } +} +``` +- Only mark `status: 'completed'` when the final resume finishes and no queued resumes remain. + +## 6. API & Background Maintenance + +### 6.1 Endpoints +- `POST /api/resume/:workflowId/:executionId/:contextId` + - Handles immediate execution or queueing of resume requests. +- `GET /api/workflows/:workflowId/paused` + - Lists paused executions and pause point metadata. +- `DELETE /api/resume/:workflowId/:executionId/:contextId` + - Cancels a pause (future work). + +### 6.2 Background Maintenance +- Scheduled task to detect `resume_queue` entries stuck in `claimed` state and mark them failed. +- Optional TTL-based cleanup for `paused_executions` (set via `expires_at`). + +## 7. Edge Cases & Scenarios + +1. **Multiple concurrent pauses** – engine collects all pause outputs before returning; single snapshot contains all pause points. Resumes may occur in any order; manager enforces sequential execution via queue. +2. **Pause within loop/parallel** – context IDs encode loop iteration and parallel branch (e.g., `pause₍1₎_loop2`). Resume restores loop and parallel scopes from snapshot so aggregation works after remaining branches finish. +3. **Nested pause-resume** – each resume execution can pause again. New paused execution rows represent the new execution ID; resume queue `parent_execution_id` links the chain. +4. **Resume while another resume running** – queued automatically; when the active execution finishes (paused or completed), the manager claims the next queued entry. +5. **Workflow modified between pause and resume** – snapshot includes serialized workflow used at pause time; resume ignores current builder state to guarantee consistency. +6. **Expired / cancelled pauses** – background task can mark rows `expired` based on `expires_at`, and API responds with 410 Gone. +7. **Failure during resume execution** – resume queue entry marked `failed`; pause remains `paused` so callers can retry. + +## 8. Implementation Plan + +### 8.1 Database Foundations +- Add migrations for `paused_executions` and `resume_queue` with indexes described in §5. +- Implement Drizzle schema + helper methods for atomic JSONB updates (resume status, timestamps). +- Provide repository utilities for locking paused executions and claiming queue entries. + +### 8.2 Type System Extensions +- Update executor types (`apps/sim/executor/types.ts`, `execution/types.ts`) to include pause metadata fields. +- Introduce shared `PauseMetadata`, `PausePoint`, `SerializedSnapshot` interfaces. +- Maintain backwards compatibility by keeping all new properties optional where practical. + +### 8.3 Handler & Executor Interfaces +- Enhance `BlockExecutor` to supply node metadata to handlers that opt in (new `executeWithNode` overload). +- Rewrite `PauseResumeBlockHandler` to emit `_pauseMetadata` using helpers in `executor/utils/pause-resume.ts`. +- Add targeted unit tests for handler output and context ID generation (parallel + loop scenarios). + +### 8.4 DAG Construction Updates +- Modify `NodeConstructor` / `EdgeConstructor` to generate `__response` and `__trigger` nodes, rewiring edges accordingly. +- Ensure resume trigger nodes are flagged (`metadata.isResumeTrigger`) so `ExecutionEngine` never seeds them initially. +- Add graph-level tests verifying pause nodes in linear, loop, and parallel configurations. + +### 8.5 Pause Utilities +- Create `pause-resume-utils.ts` with context ID generation + parsing helpers shared by handler and resume logic. +- Cover helper functions with unit tests (branch + loop naming). + +### 8.6 Snapshot Serialization Layer +- Implement `SnapshotSerializer` capable of serializing/deserializing execution context maps, loop/parallel scopes, decisions, pending queue, and trigger IDs. +- Extend `ExecutionSnapshot` to delegate to serializer and avoid redundant stringification. +- Add round-trip tests covering varied execution states. + +### 8.7 ExecutionEngine Enhancements +- Track `_pauseMetadata` during `handleNodeCompletion`, accumulate in `pausedBlocks` map. +- After queue drain, when pauses exist, generate `snapshotSeed`, populate metadata, and return `ExecutionResult` with `status: 'paused'`. +- Confirm non-pausing workflows retain original performance (benchmark/regression test). + +### 8.8 PauseResumeManager +- Implement manager module responsible for persisting pauses, enqueueing/claiming resumes, launching resume executions, and updating queue entries. +- Integrate manager with DB helpers and serializer. +- Unit-test manager logic using mocked repositories (immediate start, queueing, failures). + +### 8.9 Execution Core Integration +- Update `execution-core.ts` to: + - Call manager persistence on `status: 'paused'` results. + - Invoke new `Executor.executeFromSnapshot` helper for resume entries. + - After any run, call `manager.processQueuedResumes` to pick up pending entries sequentially. +- Ensure logging session + run-count behaviour remains unchanged for completed runs. + +### 8.10 Resume Execution Path +- Implement `Executor.executeFromSnapshot` (`apps/sim/executor/execution/executor.ts`) leveraging serializer output. +- Fill in `continueExecution` atop this helper for future tooling reuse. +- Write integration test executing pause → resume chain entirely in-memory. + +### 8.11 API & Background Maintenance +- Build POST `/api/resume/:workflowId/:executionId/:contextId` route that delegates to manager and returns queue status/position. +- Provide optional GET endpoint for listing paused executions. +- Implement scheduled cleanup job for stale `claimed` entries and expired pauses. + +### 8.12 Testing & Validation +- Unit: handler, serializer, engine pause detection, manager queue ops, DB JSON updates. +- Integration: single pause/resume, parallel pauses in shuffled order, loop pauses across iterations, nested pause chains, concurrent resume requests. +- Regression/perf: ensure non-pausing workflows match prior latency + metadata. + +### 8.13 Documentation & Operational Readiness +- Update internal docs outlining modules, data flow, resume semantics, and runbooks for stuck resumes. +- Add observability hooks (structured logs, metrics, trace tags) per §9. +- Conduct code walkthrough to validate abstractions and naming before merge. + +## 9. Observability & Operations + +- **Logging** – Structured logs from `PauseResumeManager` capturing pause creation, resume claim, completion/failure, queue length. +- **Metrics** – Counter/timer for pause hits, resume latency, queued resume depth. +- **Tracing** – Extend execution trace spans to note pause/resume transitions and resume chain depth. +- **Dashboards** – Surface number of paused executions per workspace, average resume wait time. + +## 10. Summary + +This design introduces pause-resume capability by layering new orchestration around the existing executor rather than modifying the core traversal logic. We: +- keep the engine pure and fast, +- store a single snapshot per execution with many pause points, +- reuse the same execution pipeline for resumes, +- guarantee sequential resume execution through a managed queue, +- and integrate the feature directly into the current system with strong abstractions and observability. + +With this structure in place, we can evolve towards per-branch concurrency or more advanced scheduling later without revisiting the foundational contracts established here. + +## 11. Per-Branch Pause Concurrency (Phase 2 Design) + +### 11.1 Goals + +- Allow parallel branches with independent pause blocks to continue executing until their individual queues drain without stalling sibling branches. +- Persist pause-point metadata **as soon as each pause block completes**, so the resume API can surface actionable entries while the engine finishes remaining work. +- Preserve deterministic execution: only one resume runs at a time per execution chain, but each branch can be resumed in any order once snapshots are ready. +- Minimise duplicate state by reusing the single execution snapshot captured when the engine idles, while augmenting metadata to mark which pause points are snapshot-ready. + +### 11.2 Runtime Adjustments + +1. **Immediate pause registration** + - `PauseResumeBlockHandler` invokes `PauseResumeManager.registerPausePoint` as soon as it emits `_pauseMetadata`. + - The manager lazily creates the `paused_executions` row on the **first** registration and appends additional pause point entries for subsequent blocks in the same run. + - Each pause point is recorded with `snapshotReady: false`, `resumeStatus: 'paused'`, and timestamps so the API can list forthcoming resumes immediately. + +2. **Engine completion** + - While other branches continue executing, pause metadata is accumulated both in-memory (for snapshot serialization) and in the database (for visibility). + - When the engine finally drains the queue, `handlePauseResult` updates all pause points with `snapshotReady: true` and attaches the serialized snapshot seed. This prevents resuming before the snapshot exists while satisfying the requirement that pause information is available early. + +3. **Branch-aware orchestration** + - `PauseResumeManager.registerPausePoint` records optional `parallelScope` metadata: `{ parallelId, branchIndex, branchCount }` sourced from DAG node metadata. + - On resume, this metadata enables the manager to restore only the targeted branch while leaving sibling branches paused until they are individually resumed. + +4. **Selective resume execution** + - `Executor.executeFromSnapshot` accepts a list of target trigger IDs; by default it processes a single trigger associated with the resume request. + - When a branch resumes and completes, the parallel orchestrator checks whether other branches remain paused. If so, it persists results and exits without aggregating until all branches resume; once every branch has resumed, aggregation and downstream execution proceed automatically. + +### 11.3 Database Extensions + +- `paused_executions.pause_points` entries gain additional properties: + ```json + { + "contextId": "pause₍0₎", + "resumeStatus": "paused", + "snapshotReady": false, + "parallelScope": { + "parallelId": "parallel_123", + "branchIndex": 0, + "branchCount": 3 + }, + "loopScope": { + "loopId": "loop_9", + "iteration": 2 + }, + "registeredAt": "2025-11-05T10:30:00Z", + "triggerBlockId": "pause₍0₎__trigger" + } + ``` +- When the snapshot is committed, `snapshotReady` toggles to `true` and `snapshotVersion` (e.g., the execution snapshot ID) is populated. Resume APIs should reject attempts while `snapshotReady` remains `false`. +- `paused_executions.metadata.pauseScope` transitions from `'execution'` to `'branch'` to signal the UI/API that each pause point can resume independently. + +### 11.4 API Behaviour + +- `GET /api/workflows/:workflowId/paused` now includes `snapshotReady` and `parallelScope` so clients can surface which pauses are actionable versus waiting for engine completion. +- The resume POST endpoint enforces: + - `snapshotReady === true` before queueing execution; otherwise it responds with `409 { status: 'snapshot_pending' }`. + - Standard FIFO ordering still applies—only one resume runs at a time per execution chain—but queue entries are tagged with `parallelScope` to aid operational diagnostics. + +### 11.5 Execution Flow Example + +``` +parallel { + branch0: [A] -> [pause₀] + branch1: [B] -> [pause₁] + branch2: [C] -> [pause₂] +} + +T0: branch0 hits pause → handler registers pause₀ (snapshotReady=false) +T1: branch1 hits pause → handler registers pause₁ (snapshotReady=false) +T2: branch2 hits pause → handler registers pause₂ (snapshotReady=false) +T3: no more runnable nodes → engine serializes snapshot, marks all pause points snapshotReady=true, returns status='paused' + +Resume sequence: +R1: user resumes pause₁ → manager claims queue, sets pending queue to pause₁ trigger, executes branch1 tail +R2: branch1 completion updates pause₁.resumeStatus='resumed'; parallel orchestrator records branch1 done but defers aggregation +R3: user resumes pause₀; after completion, branch0 marked resumed +R4: user resumes pause₂; with all branches complete, parallel block aggregates and upstream execution continues automatically +``` + +### 11.6 Implementation Impact Summary + +- **Manager** gains `registerPausePoint` lifecycle and updates pause rows incrementally. +- **Serializer** remains single-snapshot; we simply delay resume until the snapshot is ready. +- **Executor** continues to run one resume at a time per execution, but per-branch metadata enables clean aggregation once all paused branches resume. +- **Testing** should cover resuming branches in every permutation, validating `snapshotReady` gating and ensuring aggregation only fires after the final branch resumes. + diff --git a/apps/sim/executor/dag/types.ts b/apps/sim/executor/dag/types.ts index e88b8fed73..c4d6d388ad 100644 --- a/apps/sim/executor/dag/types.ts +++ b/apps/sim/executor/dag/types.ts @@ -7,7 +7,7 @@ export interface DAGEdge { export interface NodeMetadata { isParallelBranch?: boolean - parallelId?: string // Which parallel this branch belongs to + parallelId?: string branchIndex?: number branchTotal?: number distributionItem?: unknown diff --git a/packages/db/migrations/0106_bitter_captain_midlands.sql b/packages/db/migrations/0106_bitter_captain_midlands.sql new file mode 100644 index 0000000000..12531d8df3 --- /dev/null +++ b/packages/db/migrations/0106_bitter_captain_midlands.sql @@ -0,0 +1,37 @@ +CREATE TABLE "paused_executions" ( + "id" text PRIMARY KEY NOT NULL, + "workflow_id" text NOT NULL, + "execution_id" text NOT NULL, + "execution_snapshot" jsonb NOT NULL, + "pause_points" jsonb NOT NULL, + "total_pause_count" integer NOT NULL, + "resumed_count" integer DEFAULT 0 NOT NULL, + "status" text DEFAULT 'paused' NOT NULL, + "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL, + "paused_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "expires_at" timestamp +); +--> statement-breakpoint +CREATE TABLE "resume_queue" ( + "id" text PRIMARY KEY NOT NULL, + "paused_execution_id" text NOT NULL, + "parent_execution_id" text NOT NULL, + "new_execution_id" text NOT NULL, + "context_id" text NOT NULL, + "resume_input" jsonb, + "status" text DEFAULT 'pending' NOT NULL, + "queued_at" timestamp DEFAULT now() NOT NULL, + "claimed_at" timestamp, + "completed_at" timestamp, + "failure_reason" text +); +--> statement-breakpoint +ALTER TABLE "custom_tools" ALTER COLUMN "workspace_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "paused_executions" ADD CONSTRAINT "paused_executions_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resume_queue" ADD CONSTRAINT "resume_queue_paused_execution_id_paused_executions_id_fk" FOREIGN KEY ("paused_execution_id") REFERENCES "public"."paused_executions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "paused_executions_workflow_id_idx" ON "paused_executions" USING btree ("workflow_id");--> statement-breakpoint +CREATE INDEX "paused_executions_status_idx" ON "paused_executions" USING btree ("status");--> statement-breakpoint +CREATE UNIQUE INDEX "paused_executions_execution_id_unique" ON "paused_executions" USING btree ("execution_id");--> statement-breakpoint +CREATE INDEX "resume_queue_parent_status_idx" ON "resume_queue" USING btree ("parent_execution_id","status","queued_at");--> statement-breakpoint +CREATE INDEX "resume_queue_new_execution_idx" ON "resume_queue" USING btree ("new_execution_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0106_snapshot.json b/packages/db/migrations/meta/0106_snapshot.json new file mode 100644 index 0000000000..caf34d76ef --- /dev/null +++ b/packages/db/migrations/meta/0106_snapshot.json @@ -0,0 +1,7886 @@ +{ + "id": "64053b3a-28ff-471f-b2e9-cef259aa72ee", + "prevId": "1e942d83-6aec-4e01-a4d2-a1b8ac10e20d", + "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": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "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" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "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 + }, + "identifier": { + "name": "identifier", + "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": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "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'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "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": { + "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.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "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": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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": "set null", + "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.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_namespace_unique": { + "name": "idempotency_key_namespace_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_namespace_idx": { + "name": "idempotency_key_namespace_idx", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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\": 1, \"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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "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_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": [ + "knowledge_base_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.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "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": { + "member_user_id_idx": { + "name": "member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "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 + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_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": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": [ + "paused_execution_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": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_floating_controls": { + "name": "show_floating_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "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.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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" + } + }, + "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": false + }, + "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": { + "reference_id": { + "name": "reference_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 + }, + "api_endpoint_requests": { + "name": "api_endpoint_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": {}, + "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": false, + "default": "'10'" + }, + "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'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "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": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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 + }, + "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_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": "'{}'" + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_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": {} + } + }, + "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 + }, + "trigger_mode": { + "name": "trigger_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": "'{}'" + }, + "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_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_checkpoints": { + "name": "workflow_checkpoints", + "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 + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "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()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_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": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_id_idx": { + "name": "workflow_deployment_version_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "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_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_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 + }, + "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 + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_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_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": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_log_webhook": { + "name": "workflow_log_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "active": { + "name": "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": { + "workflow_log_webhook_workflow_id_idx": { + "name": "workflow_log_webhook_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_active_idx": { + "name": "workflow_log_webhook_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_log_webhook_workflow_id_workflow_id_fk": { + "name": "workflow_log_webhook_workflow_id_workflow_id_fk", + "tableFrom": "workflow_log_webhook", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_log_webhook_delivery": { + "name": "workflow_log_webhook_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "webhook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "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_log_webhook_delivery_subscription_id_idx": { + "name": "workflow_log_webhook_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_execution_id_idx": { + "name": "workflow_log_webhook_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_status_idx": { + "name": "workflow_log_webhook_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_next_attempt_idx": { + "name": "workflow_log_webhook_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_log_webhook_delivery_subscription_id_workflow_log_webhook_id_fk": { + "name": "workflow_log_webhook_delivery_subscription_id_workflow_log_webhook_id_fk", + "tableFrom": "workflow_log_webhook_delivery", + "tableTo": "workflow_log_webhook", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_log_webhook_delivery_workflow_id_workflow_id_fk": { + "name": "workflow_log_webhook_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workflow_log_webhook_delivery", + "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 + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "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": {}, + "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" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "billed_account_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "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": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_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 + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "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": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_files_key_unique": { + "name": "workspace_files_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "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": "workspace_invitation_status", + "typeSchema": "public", + "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'" + }, + "org_invitation_id": { + "name": "org_invitation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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" + ] + }, + "public.webhook_delivery_status": { + "name": "webhook_delivery_status", + "schema": "public", + "values": [ + "pending", + "in_progress", + "success", + "failed" + ] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": [ + "pending", + "accepted", + "rejected", + "cancelled" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index d8dfb48359..b1fd5adbc2 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -736,6 +736,13 @@ "when": 1761860659858, "tag": "0105_glamorous_wrecking_crew", "breakpoints": true + }, + { + "idx": 106, + "version": "7", + "when": 1762371130884, + "tag": "0106_bitter_captain_midlands", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 7d74e6755d..2e69635da9 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -317,6 +317,58 @@ export const workflowExecutionLogs = pgTable( }) ) +export const pausedExecutions = pgTable( + 'paused_executions', + { + id: text('id').primaryKey(), + workflowId: text('workflow_id') + .notNull() + .references(() => workflow.id, { onDelete: 'cascade' }), + executionId: text('execution_id').notNull(), + executionSnapshot: jsonb('execution_snapshot').notNull(), + pausePoints: jsonb('pause_points').notNull(), + totalPauseCount: integer('total_pause_count').notNull(), + resumedCount: integer('resumed_count').notNull().default(0), + status: text('status').notNull().default('paused'), + metadata: jsonb('metadata').notNull().default(sql`'{}'::jsonb`), + pausedAt: timestamp('paused_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + expiresAt: timestamp('expires_at'), + }, + (table) => ({ + workflowIdx: index('paused_executions_workflow_id_idx').on(table.workflowId), + statusIdx: index('paused_executions_status_idx').on(table.status), + executionUnique: uniqueIndex('paused_executions_execution_id_unique').on(table.executionId), + }) +) + +export const resumeQueue = pgTable( + 'resume_queue', + { + id: text('id').primaryKey(), + pausedExecutionId: text('paused_execution_id') + .notNull() + .references(() => pausedExecutions.id, { onDelete: 'cascade' }), + parentExecutionId: text('parent_execution_id').notNull(), + newExecutionId: text('new_execution_id').notNull(), + contextId: text('context_id').notNull(), + resumeInput: jsonb('resume_input'), + status: text('status').notNull().default('pending'), + queuedAt: timestamp('queued_at').notNull().defaultNow(), + claimedAt: timestamp('claimed_at'), + completedAt: timestamp('completed_at'), + failureReason: text('failure_reason'), + }, + (table) => ({ + parentStatusIdx: index('resume_queue_parent_status_idx').on( + table.parentExecutionId, + table.status, + table.queuedAt + ), + newExecutionIdx: index('resume_queue_new_execution_idx').on(table.newExecutionId), + }) +) + export const environment = pgTable('environment', { id: text('id').primaryKey(), // Use the user id as the key userId: text('user_id') From bd06e6b3433575904b7405aade616cc8c654c8cf Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 5 Nov 2025 11:46:55 -0800 Subject: [PATCH 04/37] Initial test passes --- apps/sim/executor/execution/block-executor.ts | 22 ++++- .../pause-resume/pause-resume-handler.test.ts | 99 +++++++++++++++++++ .../pause-resume/pause-resume-handler.ts | 60 +++++++++-- apps/sim/executor/pause-resume/utils.ts | 80 +++++++++++++++ apps/sim/executor/types.ts | 61 ++++++++++++ apps/sim/tsconfig.json | 4 +- 6 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 apps/sim/executor/handlers/pause-resume/pause-resume-handler.test.ts create mode 100644 apps/sim/executor/pause-resume/utils.ts diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 3ae571d0ba..1f85c153b2 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -47,7 +47,10 @@ export class BlockExecutor { try { resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) - const output = await handler.execute(ctx, block, resolvedInputs) + const nodeMetadata = this.buildNodeMetadata(node) + const output = handler.executeWithNode + ? await handler.executeWithNode(ctx, block, resolvedInputs, nodeMetadata) + : await handler.execute(ctx, block, resolvedInputs) const isStreamingExecution = output && typeof output === 'object' && 'stream' in output && 'execution' in output @@ -136,6 +139,23 @@ export class BlockExecutor { } } + private buildNodeMetadata(node: DAGNode): { + nodeId: string + loopId?: string + parallelId?: string + branchIndex?: number + branchTotal?: number + } { + const metadata = node?.metadata || {} + return { + nodeId: node.id, + loopId: metadata.loopId, + parallelId: metadata.parallelId, + branchIndex: metadata.branchIndex, + branchTotal: metadata.branchTotal, + } + } + private findHandler(block: SerializedBlock): BlockHandler | undefined { return this.blockHandlers.find((h) => h.canHandle(block)) } diff --git a/apps/sim/executor/handlers/pause-resume/pause-resume-handler.test.ts b/apps/sim/executor/handlers/pause-resume/pause-resume-handler.test.ts new file mode 100644 index 0000000000..95825bb136 --- /dev/null +++ b/apps/sim/executor/handlers/pause-resume/pause-resume-handler.test.ts @@ -0,0 +1,99 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PauseResumeBlockHandler } from '@/executor/handlers/pause-resume/pause-resume-handler' +import type { ExecutionContext, NormalizedBlockOutput } from '@/executor/types' + +vi.mock('@/lib/logs/console/logger', () => ({ + createLogger: vi.fn().mockReturnValue({ + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }), +})) + +describe('PauseResumeBlockHandler', () => { + let handler: PauseResumeBlockHandler + + beforeEach(() => { + handler = new PauseResumeBlockHandler() + }) + + it('returns pause metadata with parallel and loop scope information', async () => { + const executionContext: ExecutionContext = { + workflowId: 'wf_1', + blockStates: new Map(), + executedBlocks: new Set(), + blockLogs: [], + metadata: { + duration: 0, + startTime: new Date().toISOString(), + }, + environmentVariables: {}, + decisions: { + router: new Map(), + condition: new Map(), + }, + loopIterations: new Map([['loop-1', 2]]), + loopItems: new Map(), + completedLoops: new Set(), + activeExecutionPath: new Set(), + } + + const block = { + id: 'pause_block', + metadata: { id: 'pause_resume' }, + config: { params: {} }, + } as any + + const inputs = { + dataMode: 'json', + data: '{"message": "ok"}', + status: '202', + headers: [ + { + id: 'header-1', + cells: { Key: 'X-Test', Value: 'value' }, + }, + ], + } + + const nodeMetadata = { + nodeId: 'pause_block', + loopId: 'loop-1', + parallelId: 'parallel-1', + branchIndex: 1, + branchTotal: 3, + } + + const output = (await handler.executeWithNode( + executionContext, + block, + inputs, + nodeMetadata + )) as NormalizedBlockOutput + + expect(output.response).toEqual({ + data: { message: 'ok' }, + status: 202, + headers: { + 'Content-Type': 'application/json', + 'X-Test': 'value', + }, + }) + + expect(output._pauseMetadata).toBeDefined() + expect(output._pauseMetadata?.contextId).toBe('pause_block₍1₎_loop2') + expect(output._pauseMetadata?.triggerBlockId).toBe('pause_block__trigger') + expect(output._pauseMetadata?.parallelScope).toEqual({ + parallelId: 'parallel-1', + branchIndex: 1, + branchTotal: 3, + }) + expect(output._pauseMetadata?.loopScope).toEqual({ + loopId: 'loop-1', + iteration: 2, + }) + expect(output._pauseMetadata?.timestamp).toBeTypeOf('string') + }) +}) + diff --git a/apps/sim/executor/handlers/pause-resume/pause-resume-handler.ts b/apps/sim/executor/handlers/pause-resume/pause-resume-handler.ts index 94f68f03b3..c0ac361dd2 100644 --- a/apps/sim/executor/handlers/pause-resume/pause-resume-handler.ts +++ b/apps/sim/executor/handlers/pause-resume/pause-resume-handler.ts @@ -1,8 +1,13 @@ import { createLogger } from '@/lib/logs/console/logger' import type { BlockOutput } from '@/blocks/types' import { BlockType, HTTP } from '@/executor/consts' -import type { BlockHandler, ExecutionContext } from '@/executor/types' +import type { BlockHandler, ExecutionContext, PauseMetadata } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' +import { + buildTriggerBlockId, + generatePauseContextId, + mapNodeMetadataToPauseScopes, +} from '@/executor/pause-resume/utils.ts' const logger = createLogger('PauseResumeBlockHandler') @@ -23,6 +28,23 @@ export class PauseResumeBlockHandler implements BlockHandler { ctx: ExecutionContext, block: SerializedBlock, inputs: Record + ): Promise { + return this.executeWithNode(ctx, block, inputs, { + nodeId: block.id, + }) + } + + async executeWithNode( + ctx: ExecutionContext, + block: SerializedBlock, + inputs: Record, + nodeMetadata: { + nodeId: string + loopId?: string + parallelId?: string + branchIndex?: number + branchTotal?: number + } ): Promise { logger.info(`Executing pause resume block: ${block.id}`) @@ -30,19 +52,43 @@ export class PauseResumeBlockHandler implements BlockHandler { const responseData = this.parseResponseData(inputs) const statusCode = this.parseStatus(inputs.status) const responseHeaders = this.parseHeaders(inputs.headers) + const timestamp = new Date().toISOString() - logger.info('Pause resume prepared', { - status: statusCode, - dataKeys: Object.keys(responseData), - headerKeys: Object.keys(responseHeaders), - }) + const { parallelScope, loopScope } = mapNodeMetadataToPauseScopes(ctx, nodeMetadata) + const contextId = generatePauseContextId(block.id, nodeMetadata, loopScope) + const triggerBlockId = buildTriggerBlockId(nodeMetadata.nodeId) - return { + const pauseMetadata: PauseMetadata = { + contextId, + triggerBlockId, + blockId: nodeMetadata.nodeId, response: { data: responseData, status: statusCode, headers: responseHeaders, }, + timestamp, + parallelScope, + loopScope, + } + + const responseOutput = { + data: responseData, + status: statusCode, + headers: responseHeaders, + } + + logger.info('Pause resume prepared', { + status: statusCode, + contextId, + triggerBlockId, + parallelScope, + loopScope, + }) + + return { + response: responseOutput, + _pauseMetadata: pauseMetadata, } } catch (error: any) { logger.error('Pause resume block execution failed:', error) diff --git a/apps/sim/executor/pause-resume/utils.ts b/apps/sim/executor/pause-resume/utils.ts new file mode 100644 index 0000000000..14f24f66ff --- /dev/null +++ b/apps/sim/executor/pause-resume/utils.ts @@ -0,0 +1,80 @@ +import { PARALLEL } from '@/executor/consts' +import type { + ExecutionContext, + LoopPauseScope, + ParallelPauseScope, +} from '@/executor/types' + +interface NodeMetadataLike { + nodeId: string + loopId?: string + parallelId?: string + branchIndex?: number + branchTotal?: number +} + +export function generatePauseContextId( + baseBlockId: string, + nodeMetadata: NodeMetadataLike, + loopScope?: LoopPauseScope +): string { + let contextId = baseBlockId + + if (typeof nodeMetadata.branchIndex === 'number') { + contextId = `${contextId}${PARALLEL.BRANCH.PREFIX}${nodeMetadata.branchIndex}${PARALLEL.BRANCH.SUFFIX}` + } + + if (loopScope) { + contextId = `${contextId}_loop${loopScope.iteration}` + } + + return contextId +} + +export function buildTriggerBlockId(nodeId: string): string { + if (nodeId.includes('__response')) { + return nodeId.replace('__response', '__trigger') + } + + if (nodeId.endsWith('_response')) { + return nodeId.replace(/_response$/, '_trigger') + } + + return `${nodeId}__trigger` +} + +export function mapNodeMetadataToPauseScopes( + ctx: ExecutionContext, + nodeMetadata: NodeMetadataLike +): { + parallelScope?: ParallelPauseScope + loopScope?: LoopPauseScope +} { + let parallelScope: ParallelPauseScope | undefined + let loopScope: LoopPauseScope | undefined + + if ( + nodeMetadata.parallelId && + typeof nodeMetadata.branchIndex === 'number' + ) { + parallelScope = { + parallelId: nodeMetadata.parallelId, + branchIndex: nodeMetadata.branchIndex, + branchTotal: nodeMetadata.branchTotal, + } + } + + if (nodeMetadata.loopId) { + const iteration = ctx.loopIterations?.get(nodeMetadata.loopId) ?? 0 + loopScope = { + loopId: nodeMetadata.loopId, + iteration, + } + } + + return { + parallelScope, + loopScope, + } +} + diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index 12acc50cd9..adea5c30c7 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -20,6 +20,43 @@ export interface UserFile { /** * Standardized block output format that ensures compatibility with the execution engine. */ +export interface ParallelPauseScope { + parallelId: string + branchIndex: number + branchTotal?: number +} + +export interface LoopPauseScope { + loopId: string + iteration: number +} + +export interface PauseMetadata { + contextId: string + triggerBlockId: string + blockId: string + response: any + timestamp: string + parallelScope?: ParallelPauseScope + loopScope?: LoopPauseScope +} + +export interface PausePoint { + contextId: string + triggerBlockId: string + response: any + registeredAt: string + resumeStatus: 'paused' | 'resumed' | 'failed' + snapshotReady: boolean + parallelScope?: ParallelPauseScope + loopScope?: LoopPauseScope +} + +export interface SerializedSnapshot { + snapshot: string + triggerIds: string[] +} + export interface NormalizedBlockOutput { [key: string]: any // Content fields @@ -57,6 +94,8 @@ export interface NormalizedBlockOutput { // Child workflow introspection (for workflow blocks) childTraceSpans?: TraceSpan[] childWorkflowName?: string + // Pause metadata + _pauseMetadata?: PauseMetadata } /** @@ -89,6 +128,12 @@ export interface ExecutionMetadata { isDebugSession?: boolean // Whether the workflow is running in debug mode context?: ExecutionContext // Runtime context for the workflow workflowConnections?: Array<{ source: string; target: string }> // Connections between workflow blocks + status?: 'running' | 'paused' | 'completed' + pausePoints?: string[] + resumeChain?: { + parentExecutionId?: string + depth: number + } } /** @@ -199,6 +244,9 @@ export interface ExecutionResult { error?: string // Error message if execution failed logs?: BlockLog[] // Execution logs for all blocks metadata?: ExecutionMetadata + status?: 'completed' | 'paused' + pausePoints?: PausePoint[] + snapshotSeed?: SerializedSnapshot _streamingMetadata?: { // Internal metadata for streaming execution loggingSession: any @@ -252,6 +300,19 @@ export interface BlockHandler { block: SerializedBlock, inputs: Record ): Promise + + executeWithNode?: ( + ctx: ExecutionContext, + block: SerializedBlock, + inputs: Record, + nodeMetadata: { + nodeId: string + loopId?: string + parallelId?: string + branchIndex?: number + branchTotal?: number + } + ) => Promise } /** diff --git a/apps/sim/tsconfig.json b/apps/sim/tsconfig.json index 0ac3794dc8..67a871a6ee 100644 --- a/apps/sim/tsconfig.json +++ b/apps/sim/tsconfig.json @@ -27,7 +27,9 @@ "@sim/db": ["../../packages/db"], "@sim/db/*": ["../../packages/db/*"], "@/executor": ["./executor"], - "@/executor/*": ["./executor/*"] + "@/executor/*": ["./executor/*"], + "@/executor/pause-resume": ["./executor/pause-resume"], + "@/executor/pause-resume/*": ["./executor/pause-resume/*"] }, "allowJs": true, "noEmit": true, From 224b9b1af0998cc3544c5865ed48f2e042d96bff Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 5 Nov 2025 12:00:59 -0800 Subject: [PATCH 05/37] Tests pass --- .../executor/dag/builder.pause-resume.test.ts | 74 +++++++++++++++ apps/sim/executor/dag/builder.ts | 11 ++- apps/sim/executor/dag/construction/edges.ts | 30 ++++-- apps/sim/executor/dag/construction/nodes.ts | 92 +++++++++++++++++-- apps/sim/executor/dag/types.ts | 3 + apps/sim/executor/execution/engine.ts | 80 +++++++++++++++- .../executor/execution/snapshot-serializer.ts | 66 +++++++++++++ 7 files changed, 335 insertions(+), 21 deletions(-) create mode 100644 apps/sim/executor/dag/builder.pause-resume.test.ts create mode 100644 apps/sim/executor/execution/snapshot-serializer.ts diff --git a/apps/sim/executor/dag/builder.pause-resume.test.ts b/apps/sim/executor/dag/builder.pause-resume.test.ts new file mode 100644 index 0000000000..9dc60bb8cf --- /dev/null +++ b/apps/sim/executor/dag/builder.pause-resume.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it, vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import { DAGBuilder } from '@/executor/dag/builder' +import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' + +vi.mock('@/lib/logs/console/logger', () => ({ + createLogger: vi.fn().mockReturnValue({ + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }), +})) + +function createBlock(id: string, metadataId: string): SerializedBlock { + return { + id, + position: { x: 0, y: 0 }, + config: { + tool: 'noop', + params: {}, + }, + inputs: {}, + outputs: {}, + metadata: { + id: metadataId, + name: id, + }, + enabled: true, + } +} + +describe('DAGBuilder pause-resume transformation', () => { + it('creates trigger nodes and rewires edges for pause blocks', () => { + const workflow: SerializedWorkflow = { + version: '1', + blocks: [ + createBlock('start', BlockType.STARTER), + createBlock('pause', BlockType.PAUSE_RESUME), + createBlock('finish', BlockType.FUNCTION), + ], + connections: [ + { source: 'start', target: 'pause' }, + { source: 'pause', target: 'finish' }, + ], + loops: {}, + } + + const builder = new DAGBuilder() + const dag = builder.build(workflow) + + const pauseNode = dag.nodes.get('pause') + expect(pauseNode).toBeDefined() + expect(pauseNode?.metadata.isPauseResponse).toBe(true) + + const triggerNode = dag.nodes.get('pause__trigger') + expect(triggerNode).toBeDefined() + expect(triggerNode?.metadata.isResumeTrigger).toBe(true) + + const startNode = dag.nodes.get('start')! + const startOutgoing = Array.from(startNode.outgoingEdges.values()) + expect(startOutgoing).toHaveLength(1) + expect(startOutgoing[0].target).toBe('pause') + + const pauseOutgoing = Array.from(pauseNode!.outgoingEdges.values()) + expect(pauseOutgoing).toHaveLength(0) + + const triggerOutgoing = Array.from(triggerNode!.outgoingEdges.values()) + expect(triggerOutgoing).toHaveLength(1) + expect(triggerOutgoing[0].target).toBe('finish') + }) +}) + + diff --git a/apps/sim/executor/dag/builder.ts b/apps/sim/executor/dag/builder.ts index 0922406fcc..603f7de6b7 100644 --- a/apps/sim/executor/dag/builder.ts +++ b/apps/sim/executor/dag/builder.ts @@ -51,13 +51,20 @@ export class DAGBuilder { this.loopConstructor.execute(dag, reachableBlocks) - const { blocksInLoops, blocksInParallels } = this.nodeConstructor.execute( + const { blocksInLoops, blocksInParallels, pauseTriggerMapping } = this.nodeConstructor.execute( workflow, dag, reachableBlocks ) - this.edgeConstructor.execute(workflow, dag, blocksInParallels, blocksInLoops, reachableBlocks) + this.edgeConstructor.execute( + workflow, + dag, + blocksInParallels, + blocksInLoops, + reachableBlocks, + pauseTriggerMapping + ) logger.info('DAG built', { totalNodes: dag.nodes.size, diff --git a/apps/sim/executor/dag/construction/edges.ts b/apps/sim/executor/dag/construction/edges.ts index 8e6b4b0531..ab768f790f 100644 --- a/apps/sim/executor/dag/construction/edges.ts +++ b/apps/sim/executor/dag/construction/edges.ts @@ -31,7 +31,8 @@ export class EdgeConstructor { dag: DAG, blocksInParallels: Set, blocksInLoops: Set, - reachableBlocks: Set + reachableBlocks: Set, + pauseTriggerMapping: Map ): void { const loopBlockIds = new Set(dag.loopConfigs.keys()) const parallelBlockIds = new Set(dag.parallelConfigs.keys()) @@ -44,10 +45,11 @@ export class EdgeConstructor { reachableBlocks, loopBlockIds, parallelBlockIds, - metadata + metadata, + pauseTriggerMapping ) this.wireLoopSentinels(dag, reachableBlocks) - this.wireParallelBlocks(workflow, dag, loopBlockIds, parallelBlockIds) + this.wireParallelBlocks(workflow, dag, loopBlockIds, parallelBlockIds, pauseTriggerMapping) } private buildMetadataMaps(workflow: SerializedWorkflow): EdgeMetadata { @@ -122,10 +124,12 @@ export class EdgeConstructor { reachableBlocks: Set, loopBlockIds: Set, parallelBlockIds: Set, - metadata: EdgeMetadata + metadata: EdgeMetadata, + pauseTriggerMapping: Map ): void { for (const connection of workflow.connections) { let { source, target } = connection + const originalSource = source let sourceHandle = this.generateSourceHandle( source, target, @@ -185,7 +189,8 @@ export class EdgeConstructor { sourceParallelId!, dag, sourceHandle, - targetHandle + targetHandle, + pauseTriggerMapping ) } else { logger.warn('Edge between different parallels - invalid workflow', { source, target }) @@ -196,7 +201,8 @@ export class EdgeConstructor { target, }) } else { - this.addEdge(dag, source, target, sourceHandle, targetHandle) + const resolvedSource = pauseTriggerMapping.get(originalSource) ?? source + this.addEdge(dag, resolvedSource, target, sourceHandle, targetHandle) } } } @@ -232,7 +238,8 @@ export class EdgeConstructor { workflow: SerializedWorkflow, dag: DAG, loopBlockIds: Set, - parallelBlockIds: Set + parallelBlockIds: Set, + pauseTriggerMapping: Map ): void { for (const [parallelId, parallelConfig] of dag.parallelConfigs) { const nodes = parallelConfig.nodes @@ -283,7 +290,8 @@ export class EdgeConstructor { for (let i = 0; i < branchCount; i++) { const branchNodeId = buildBranchNodeId(terminalNodeId, i) if (dag.nodes.has(branchNodeId)) { - this.addEdge(dag, branchNodeId, target, sourceHandle, targetHandle) + const resolvedSourceId = pauseTriggerMapping.get(branchNodeId) ?? branchNodeId + this.addEdge(dag, resolvedSourceId, target, sourceHandle, targetHandle) } } } @@ -340,7 +348,8 @@ export class EdgeConstructor { parallelId: string, dag: DAG, sourceHandle?: string, - targetHandle?: string + targetHandle?: string, + pauseTriggerMapping?: Map ): void { const parallelConfig = dag.parallelConfigs.get(parallelId) if (!parallelConfig) { @@ -351,7 +360,8 @@ export class EdgeConstructor { for (let i = 0; i < count; i++) { const sourceNodeId = buildBranchNodeId(source, i) const targetNodeId = buildBranchNodeId(target, i) - this.addEdge(dag, sourceNodeId, targetNodeId, sourceHandle, targetHandle) + const resolvedSourceId = pauseTriggerMapping?.get(sourceNodeId) ?? sourceNodeId + this.addEdge(dag, resolvedSourceId, targetNodeId, sourceHandle, targetHandle) } } diff --git a/apps/sim/executor/dag/construction/nodes.ts b/apps/sim/executor/dag/construction/nodes.ts index 39478f7a04..2d29f148e8 100644 --- a/apps/sim/executor/dag/construction/nodes.ts +++ b/apps/sim/executor/dag/construction/nodes.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { isMetadataOnlyBlockType } from '@/executor/consts' +import { BlockType, isMetadataOnlyBlockType } from '@/executor/consts' import { buildBranchNodeId, calculateBranchCount, @@ -20,9 +20,14 @@ export class NodeConstructor { workflow: SerializedWorkflow, dag: DAG, reachableBlocks: Set - ): { blocksInLoops: Set; blocksInParallels: Set } { + ): { + blocksInLoops: Set + blocksInParallels: Set + pauseTriggerMapping: Map + } { const blocksInLoops = new Set() const blocksInParallels = new Set() + const pauseTriggerMapping = new Map() this.categorizeBlocks(dag, reachableBlocks, blocksInLoops, blocksInParallels) for (const block of workflow.blocks) { if (!this.shouldProcessBlock(block, reachableBlocks)) { @@ -30,12 +35,12 @@ export class NodeConstructor { } const parallelId = this.findParallelForBlock(block.id, dag) if (parallelId) { - this.createParallelBranchNodes(block, parallelId, dag) + this.createParallelBranchNodes(block, parallelId, dag, pauseTriggerMapping) } else { - this.createRegularOrLoopNode(block, blocksInLoops, dag) + this.createRegularOrLoopNode(block, blocksInLoops, dag, pauseTriggerMapping) } } - return { blocksInLoops, blocksInParallels } + return { blocksInLoops, blocksInParallels, pauseTriggerMapping } } private shouldProcessBlock(block: SerializedBlock, reachableBlocks: Set): boolean { @@ -94,7 +99,12 @@ export class NodeConstructor { } } - private createParallelBranchNodes(block: SerializedBlock, parallelId: string, dag: DAG): void { + private createParallelBranchNodes( + block: SerializedBlock, + parallelId: string, + dag: DAG, + pauseTriggerMapping: Map + ): void { const expansion = this.calculateParallelExpansion(parallelId, dag) logger.debug('Creating parallel branches', { blockId: block.id, @@ -104,6 +114,19 @@ export class NodeConstructor { for (let branchIndex = 0; branchIndex < expansion.branchCount; branchIndex++) { const branchNode = this.createParallelBranchNode(block, branchIndex, expansion) dag.nodes.set(branchNode.id, branchNode) + + if (block.metadata?.id === BlockType.PAUSE_RESUME) { + const triggerId = `${branchNode.id}__trigger` + const triggerNode = this.createTriggerNode(block, triggerId, { + isParallelBranch: true, + parallelId: expansion.parallelId, + branchIndex, + branchTotal: expansion.branchCount, + loopId: branchNode.metadata.loopId, + }) + dag.nodes.set(triggerId, triggerNode) + pauseTriggerMapping.set(branchNode.id, triggerId) + } } } @@ -127,9 +150,13 @@ export class NodeConstructor { expansion: ParallelExpansion ): DAGNode { const branchNodeId = buildBranchNodeId(baseBlock.id, branchIndex) + const blockClone: SerializedBlock = { + ...baseBlock, + id: branchNodeId, + } return { id: branchNodeId, - block: { ...baseBlock }, + block: blockClone, incomingEdges: new Set(), outgoingEdges: new Map(), metadata: { @@ -138,6 +165,8 @@ export class NodeConstructor { branchIndex, branchTotal: expansion.branchCount, distributionItem: expansion.distributionItems[branchIndex], + isPauseResponse: baseBlock.metadata?.id === BlockType.PAUSE_RESUME, + originalBlockId: baseBlock.id, }, } } @@ -145,10 +174,12 @@ export class NodeConstructor { private createRegularOrLoopNode( block: SerializedBlock, blocksInLoops: Set, - dag: DAG + dag: DAG, + pauseTriggerMapping: Map ): void { const isLoopNode = blocksInLoops.has(block.id) const loopId = isLoopNode ? this.findLoopIdForBlock(block.id, dag) : undefined + const isPauseBlock = block.metadata?.id === BlockType.PAUSE_RESUME dag.nodes.set(block.id, { id: block.id, block, @@ -157,8 +188,53 @@ export class NodeConstructor { metadata: { isLoopNode, loopId, + isPauseResponse: isPauseBlock, + originalBlockId: block.id, }, }) + + if (isPauseBlock) { + const triggerId = `${block.id}__trigger` + const triggerNode = this.createTriggerNode(block, triggerId, { + loopId, + }) + dag.nodes.set(triggerId, triggerNode) + pauseTriggerMapping.set(block.id, triggerId) + } + } + + private createTriggerNode( + block: SerializedBlock, + triggerId: string, + options: { + loopId?: string + isParallelBranch?: boolean + parallelId?: string + branchIndex?: number + branchTotal?: number + } + ): DAGNode { + const triggerBlock: SerializedBlock = { + ...block, + id: triggerId, + enabled: true, + } + + return { + id: triggerId, + block: triggerBlock, + incomingEdges: new Set(), + outgoingEdges: new Map(), + metadata: { + isResumeTrigger: true, + originalBlockId: block.id, + loopId: options.loopId, + isParallelBranch: options.isParallelBranch, + parallelId: options.parallelId, + branchIndex: options.branchIndex, + branchTotal: options.branchTotal, + }, + } } private findLoopIdForBlock(blockId: string, dag: DAG): string | undefined { diff --git a/apps/sim/executor/dag/types.ts b/apps/sim/executor/dag/types.ts index c4d6d388ad..cbd1374d40 100644 --- a/apps/sim/executor/dag/types.ts +++ b/apps/sim/executor/dag/types.ts @@ -15,4 +15,7 @@ export interface NodeMetadata { loopId?: string isSentinel?: boolean sentinelType?: 'start' | 'end' + isPauseResponse?: boolean + isResumeTrigger?: boolean + originalBlockId?: string } diff --git a/apps/sim/executor/execution/engine.ts b/apps/sim/executor/execution/engine.ts index 9f7c64e9bb..dfc27c7884 100644 --- a/apps/sim/executor/execution/engine.ts +++ b/apps/sim/executor/execution/engine.ts @@ -1,6 +1,13 @@ import { createLogger } from '@/lib/logs/console/logger' import { BlockType } from '@/executor/consts' -import type { ExecutionContext, ExecutionResult, NormalizedBlockOutput } from '@/executor/types' +import type { + ExecutionContext, + ExecutionResult, + NormalizedBlockOutput, + PauseMetadata, + PausePoint, +} from '@/executor/types' +import { serializePauseSnapshot } from '@/executor/execution/snapshot-serializer' import type { DAG } from '../dag/builder' import type { NodeExecutionOrchestrator } from '../orchestrators/node' import type { EdgeManager } from './edge-manager' @@ -12,6 +19,7 @@ export class ExecutionEngine { private executing = new Set>() private queueLock = Promise.resolve() private finalOutput: NormalizedBlockOutput = {} + private pausedBlocks: Map = new Map() constructor( private dag: DAG, @@ -38,6 +46,10 @@ export class ExecutionEngine { }) await this.waitForAllExecutions() + if (this.pausedBlocks.size > 0) { + return this.buildPausedResult(startTime) + } + const endTime = Date.now() this.context.metadata.endTime = new Date(endTime).toISOString() this.context.metadata.duration = endTime - startTime @@ -74,6 +86,12 @@ export class ExecutionEngine { } private addToQueue(nodeId: string): void { + const node = this.dag.nodes.get(nodeId) + if (node?.metadata?.isResumeTrigger) { + logger.debug('Skipping enqueue for resume trigger node', { nodeId }) + return + } + if (!this.readyQueue.includes(nodeId)) { this.readyQueue.push(nodeId) logger.debug('Added to queue', { nodeId, queueLength: this.readyQueue.length }) @@ -183,6 +201,21 @@ export class ExecutionEngine { return } + if (output._pauseMetadata) { + const pauseMetadata = output._pauseMetadata + this.pausedBlocks.set(pauseMetadata.contextId, pauseMetadata) + this.context.metadata.status = 'paused' + this.context.metadata.pausePoints = Array.from(this.pausedBlocks.keys()) + + logger.debug('Registered pause metadata', { + nodeId, + contextId: pauseMetadata.contextId, + triggerBlockId: pauseMetadata.triggerBlockId, + }) + + return + } + await this.nodeOrchestrator.handleNodeCompletion(nodeId, output, this.context) if (isFinalOutput) { @@ -198,4 +231,49 @@ export class ExecutionEngine { queueSize: this.readyQueue.length, }) } + + private buildPausedResult(startTime: number): ExecutionResult { + const endTime = Date.now() + this.context.metadata.endTime = new Date(endTime).toISOString() + this.context.metadata.duration = endTime - startTime + this.context.metadata.status = 'paused' + + const triggerIds = Array.from(this.pausedBlocks.values()).map( + (pause) => pause.triggerBlockId + ) + const snapshotSeed = serializePauseSnapshot(this.context, triggerIds) + const pausePoints: PausePoint[] = Array.from(this.pausedBlocks.values()).map((pause) => ({ + contextId: pause.contextId, + triggerBlockId: pause.triggerBlockId, + response: pause.response, + registeredAt: pause.timestamp, + resumeStatus: 'paused', + snapshotReady: true, + parallelScope: pause.parallelScope, + loopScope: pause.loopScope, + })) + + return { + success: true, + output: this.collectPauseResponses(), + logs: this.context.blockLogs, + metadata: this.context.metadata, + status: 'paused', + pausePoints, + snapshotSeed, + } + } + + private collectPauseResponses(): NormalizedBlockOutput { + const responses = Array.from(this.pausedBlocks.values()).map((pause) => pause.response) + + if (responses.length === 1) { + return responses[0] + } + + return { + pausedBlocks: responses, + pauseCount: responses.length, + } + } } diff --git a/apps/sim/executor/execution/snapshot-serializer.ts b/apps/sim/executor/execution/snapshot-serializer.ts new file mode 100644 index 0000000000..46489497d5 --- /dev/null +++ b/apps/sim/executor/execution/snapshot-serializer.ts @@ -0,0 +1,66 @@ +import { ExecutionSnapshot } from '@/executor/execution/snapshot' +import type { SerializableExecutionState } from '@/executor/execution/snapshot' +import type { ExecutionContext, SerializedSnapshot } from '@/executor/types' + +function mapFromEntries(map?: Map): Record | undefined { + if (!map) return undefined + return Object.fromEntries(map) +} + +function setToArray(set?: Set): T[] | undefined { + if (!set) return undefined + return Array.from(set) +} + +export function serializePauseSnapshot( + context: ExecutionContext, + triggerBlockIds: string[] +): SerializedSnapshot { + const state: SerializableExecutionState = { + blockStates: Object.fromEntries(context.blockStates), + executedBlocks: Array.from(context.executedBlocks), + blockLogs: context.blockLogs, + decisions: { + router: Object.fromEntries(context.decisions.router), + condition: Object.fromEntries(context.decisions.condition), + }, + loopIterations: Object.fromEntries(context.loopIterations), + loopItems: Object.fromEntries(context.loopItems), + completedLoops: Array.from(context.completedLoops), + loopExecutions: mapFromEntries(context.loopExecutions), + parallelExecutions: mapFromEntries(context.parallelExecutions), + parallelBlockMapping: mapFromEntries(context.parallelBlockMapping), + activeExecutionPath: Array.from(context.activeExecutionPath), + pendingQueue: triggerBlockIds, + } + + const executionMetadata = { + requestId: + (context.metadata as any)?.requestId ?? context.executionId ?? context.workflowId ?? 'unknown', + executionId: context.executionId ?? 'unknown', + workflowId: context.workflowId, + workspaceId: context.workspaceId, + userId: (context.metadata as any)?.userId ?? '', + triggerType: (context.metadata as any)?.triggerType ?? 'manual', + triggerBlockId: triggerBlockIds[0], + useDraftState: false, + startTime: context.metadata.startTime ?? new Date().toISOString(), + } + + const snapshot = new ExecutionSnapshot( + executionMetadata, + context.workflow, + {}, + context.environmentVariables, + context.workflowVariables || {}, + context.selectedOutputs || [], + state + ) + + return { + snapshot: snapshot.toJSON(), + triggerIds: triggerBlockIds, + } +} + + From 99b27dea46832d885155e29503962b545dc3e27a Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 5 Nov 2025 12:05:44 -0800 Subject: [PATCH 06/37] Execution pauses --- .../lib/workflows/executor/execution-core.ts | 18 ++++- .../executor/pause-resume-manager.ts | 69 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 apps/sim/lib/workflows/executor/pause-resume-manager.ts diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index c5b8722510..bc9722ed86 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -22,6 +22,7 @@ import type { ExecutionCallbacks, ExecutionSnapshot } from '@/executor/execution import type { ExecutionResult } from '@/executor/types' import { Serializer } from '@/serializer' import { mergeSubblockState } from '@/stores/workflows/server-utils' +import { PauseResumeManager } from './pause-resume-manager' const logger = createLogger('ExecutionCore') @@ -306,11 +307,26 @@ export async function executeWorkflowCore( resolvedTriggerBlockId )) as ExecutionResult + if (result.status === 'paused') { + if (!result.snapshotSeed) { + logger.error(`[${requestId}] Missing snapshot seed for paused execution`, { + executionId, + }) + } else { + await PauseResumeManager.persistPauseResult({ + workflowId, + executionId, + pausePoints: result.pausePoints || [], + snapshotSeed: result.snapshotSeed, + }) + } + } + // Build trace spans for logging const { traceSpans, totalDuration } = buildTraceSpans(result) // Update workflow run counts - if (result.success) { + if (result.success && result.status !== 'paused') { await updateWorkflowRunCounts(workflowId) } diff --git a/apps/sim/lib/workflows/executor/pause-resume-manager.ts b/apps/sim/lib/workflows/executor/pause-resume-manager.ts new file mode 100644 index 0000000000..6081fe4c31 --- /dev/null +++ b/apps/sim/lib/workflows/executor/pause-resume-manager.ts @@ -0,0 +1,69 @@ +import { v4 as uuidv4 } from 'uuid' +import { db } from '@sim/db' +import { pausedExecutions } from '@sim/db/schema' +import type { PausePoint, SerializedSnapshot } from '@/executor/types' + +interface PersistPauseResultArgs { + workflowId: string + executionId: string + pausePoints: PausePoint[] + snapshotSeed: SerializedSnapshot +} + +export class PauseResumeManager { + static async persistPauseResult(args: PersistPauseResultArgs): Promise { + const { workflowId, executionId, pausePoints, snapshotSeed } = args + + const pausePointsRecord = pausePoints.reduce>((acc, point) => { + acc[point.contextId] = { + contextId: point.contextId, + triggerBlockId: point.triggerBlockId, + response: point.response, + resumeStatus: point.resumeStatus, + snapshotReady: point.snapshotReady, + registeredAt: point.registeredAt, + parallelScope: point.parallelScope, + loopScope: point.loopScope, + } + return acc + }, {}) + + const now = new Date() + + await db + .insert(pausedExecutions) + .values({ + id: uuidv4(), + workflowId, + executionId, + executionSnapshot: snapshotSeed, + pausePoints: pausePointsRecord, + totalPauseCount: pausePoints.length, + resumedCount: 0, + status: 'paused', + metadata: { + pauseScope: 'execution', + triggerIds: snapshotSeed.triggerIds, + }, + pausedAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: pausedExecutions.executionId, + set: { + executionSnapshot: snapshotSeed, + pausePoints: pausePointsRecord, + totalPauseCount: pausePoints.length, + resumedCount: 0, + status: 'paused', + metadata: { + pauseScope: 'execution', + triggerIds: snapshotSeed.triggerIds, + }, + updatedAt: now, + }, + }) + } +} + + From 0f27f244596e47007d9d9ff0dc6a1921de29fc33 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 5 Nov 2025 12:20:37 -0800 Subject: [PATCH 07/37] Snapshot serializer --- .../app/api/workflows/[id]/execute/route.ts | 31 ++ apps/sim/background/schedule-execution.ts | 16 + apps/sim/background/webhook-execution.ts | 31 ++ apps/sim/background/workflow-execution.ts | 16 + .../executor/execution/snapshot-serializer.ts | 2 +- .../lib/workflows/executor/execution-core.ts | 16 - .../executor/pause-resume-manager.ts | 346 +++++++++++++++++- 7 files changed, 439 insertions(+), 19 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index e80cbd6ecd..3ebba186c2 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -6,6 +6,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { LoggingSession } from '@/lib/logs/execution/logging-session' import { generateRequestId, SSE_HEADERS } from '@/lib/utils' import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core' +import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager' import { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events' import { validateWorkflowAccess } from '@/app/api/workflows/middleware' import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/snapshot' @@ -129,6 +130,21 @@ export async function executeWorkflow( loggingSession, }) + if (result.status === 'paused') { + if (!result.snapshotSeed) { + logger.error(`[${requestId}] Missing snapshot seed for paused execution`, { + executionId, + }) + } else { + await PauseResumeManager.persistPauseResult({ + workflowId, + executionId, + pausePoints: result.pausePoints || [], + snapshotSeed: result.snapshotSeed, + }) + } + } + if (streamConfig?.skipLoggingComplete) { return { ...result, @@ -544,6 +560,21 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: loggingSession, }) + if (result.status === 'paused') { + if (!result.snapshotSeed) { + logger.error(`[${requestId}] Missing snapshot seed for paused execution`, { + executionId, + }) + } else { + await PauseResumeManager.persistPauseResult({ + workflowId, + executionId, + pausePoints: result.pausePoints || [], + snapshotSeed: result.snapshotSeed, + }) + } + } + if (result.error === 'Workflow execution was cancelled') { logger.info(`[${requestId}] Workflow execution was cancelled`) sendEvent({ diff --git a/apps/sim/background/schedule-execution.ts b/apps/sim/background/schedule-execution.ts index 7fbf7dd31d..a178c47956 100644 --- a/apps/sim/background/schedule-execution.ts +++ b/apps/sim/background/schedule-execution.ts @@ -17,6 +17,7 @@ import { import { decryptSecret } from '@/lib/utils' import { blockExistsInDeployment, loadDeployedWorkflowState } from '@/lib/workflows/db-helpers' import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core' +import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager' import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/snapshot' import { Serializer } from '@/serializer' @@ -452,6 +453,21 @@ export async function executeScheduleJob(payload: ScheduleExecutionPayload) { loggingSession, }) + if (executionResult.status === 'paused') { + if (!executionResult.snapshotSeed) { + logger.error(`[${requestId}] Missing snapshot seed for paused execution`, { + executionId, + }) + } else { + await PauseResumeManager.persistPauseResult({ + workflowId: payload.workflowId, + executionId, + pausePoints: executionResult.pausePoints || [], + snapshotSeed: executionResult.snapshotSeed, + }) + } + } + logger.info(`[${requestId}] Workflow execution completed: ${payload.workflowId}`, { success: executionResult.success, executionTime: executionResult.metadata?.duration, diff --git a/apps/sim/background/webhook-execution.ts b/apps/sim/background/webhook-execution.ts index 2dc7a905f7..1fe2169e95 100644 --- a/apps/sim/background/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -23,6 +23,7 @@ import type { ExecutionResult } from '@/executor/types' import { Serializer } from '@/serializer' import { mergeSubblockState } from '@/stores/workflows/server-utils' import { getTrigger, isTriggerValid } from '@/triggers' +import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager' const logger = createLogger('TriggerWebhookExecution') @@ -274,6 +275,21 @@ async function executeWebhookJobInternal( loggingSession, }) + if (executionResult.status === 'paused') { + if (!executionResult.snapshotSeed) { + logger.error(`[${requestId}] Missing snapshot seed for paused execution`, { + executionId, + }) + } else { + await PauseResumeManager.persistPauseResult({ + workflowId: payload.workflowId, + executionId, + pausePoints: executionResult.pausePoints || [], + snapshotSeed: executionResult.snapshotSeed, + }) + } + } + logger.info(`[${requestId}] Airtable webhook execution completed`, { success: executionResult.success, workflowId: payload.workflowId, @@ -460,6 +476,21 @@ async function executeWebhookJobInternal( loggingSession, }) + if (executionResult.status === 'paused') { + if (!executionResult.snapshotSeed) { + logger.error(`[${requestId}] Missing snapshot seed for paused execution`, { + executionId, + }) + } else { + await PauseResumeManager.persistPauseResult({ + workflowId: payload.workflowId, + executionId, + pausePoints: executionResult.pausePoints || [], + snapshotSeed: executionResult.snapshotSeed, + }) + } + } + logger.info(`[${requestId}] Webhook execution completed`, { success: executionResult.success, workflowId: payload.workflowId, diff --git a/apps/sim/background/workflow-execution.ts b/apps/sim/background/workflow-execution.ts index de83deef52..dd94e1c2e8 100644 --- a/apps/sim/background/workflow-execution.ts +++ b/apps/sim/background/workflow-execution.ts @@ -7,6 +7,7 @@ import { checkServerSideUsageLimits } from '@/lib/billing' import { createLogger } from '@/lib/logs/console/logger' import { LoggingSession } from '@/lib/logs/execution/logging-session' import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core' +import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager' import { getWorkflowById } from '@/lib/workflows/utils' import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/snapshot' @@ -119,6 +120,21 @@ export async function executeWorkflowJob(payload: WorkflowExecutionPayload) { loggingSession, }) + if (result.status === 'paused') { + if (!result.snapshotSeed) { + logger.error(`[${requestId}] Missing snapshot seed for paused execution`, { + executionId, + }) + } else { + await PauseResumeManager.persistPauseResult({ + workflowId, + executionId, + pausePoints: result.pausePoints || [], + snapshotSeed: result.snapshotSeed, + }) + } + } + logger.info(`[${requestId}] Workflow execution completed: ${workflowId}`, { success: result.success, executionTime: result.metadata?.duration, diff --git a/apps/sim/executor/execution/snapshot-serializer.ts b/apps/sim/executor/execution/snapshot-serializer.ts index 46489497d5..f76c87138a 100644 --- a/apps/sim/executor/execution/snapshot-serializer.ts +++ b/apps/sim/executor/execution/snapshot-serializer.ts @@ -51,7 +51,7 @@ export function serializePauseSnapshot( executionMetadata, context.workflow, {}, - context.environmentVariables, + {}, context.workflowVariables || {}, context.selectedOutputs || [], state diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index bc9722ed86..b566b35303 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -22,7 +22,6 @@ import type { ExecutionCallbacks, ExecutionSnapshot } from '@/executor/execution import type { ExecutionResult } from '@/executor/types' import { Serializer } from '@/serializer' import { mergeSubblockState } from '@/stores/workflows/server-utils' -import { PauseResumeManager } from './pause-resume-manager' const logger = createLogger('ExecutionCore') @@ -307,21 +306,6 @@ export async function executeWorkflowCore( resolvedTriggerBlockId )) as ExecutionResult - if (result.status === 'paused') { - if (!result.snapshotSeed) { - logger.error(`[${requestId}] Missing snapshot seed for paused execution`, { - executionId, - }) - } else { - await PauseResumeManager.persistPauseResult({ - workflowId, - executionId, - pausePoints: result.pausePoints || [], - snapshotSeed: result.snapshotSeed, - }) - } - } - // Build trace spans for logging const { traceSpans, totalDuration } = buildTraceSpans(result) diff --git a/apps/sim/lib/workflows/executor/pause-resume-manager.ts b/apps/sim/lib/workflows/executor/pause-resume-manager.ts index 6081fe4c31..6d9f702666 100644 --- a/apps/sim/lib/workflows/executor/pause-resume-manager.ts +++ b/apps/sim/lib/workflows/executor/pause-resume-manager.ts @@ -1,7 +1,11 @@ import { v4 as uuidv4 } from 'uuid' +import { sql, eq, and, inArray, lt } from 'drizzle-orm' import { db } from '@sim/db' -import { pausedExecutions } from '@sim/db/schema' +import { pausedExecutions, resumeQueue } from '@sim/db/schema' import type { PausePoint, SerializedSnapshot } from '@/executor/types' +import { ExecutionSnapshot } from '@/executor/execution/snapshot' +import { LoggingSession } from '@/lib/logs/execution/logging-session' +import { executeWorkflowCore } from './execution-core' interface PersistPauseResultArgs { workflowId: string @@ -10,6 +14,38 @@ interface PersistPauseResultArgs { snapshotSeed: SerializedSnapshot } +interface EnqueueResumeArgs { + executionId: string + contextId: string + resumeInput: any + userId: string +} + +type EnqueueResumeResult = + | { + status: 'queued' + resumeExecutionId: string + queuePosition: number + } + | { + status: 'starting' + resumeExecutionId: string + resumeEntryId: string + pausedExecution: typeof pausedExecutions.$inferSelect + contextId: string + resumeInput: any + userId: string + } + +interface StartResumeExecutionArgs { + resumeEntryId: string + resumeExecutionId: string + pausedExecution: typeof pausedExecutions.$inferSelect + contextId: string + resumeInput: any + userId: string +} + export class PauseResumeManager { static async persistPauseResult(args: PersistPauseResultArgs): Promise { const { workflowId, executionId, pausePoints, snapshotSeed } = args @@ -63,7 +99,313 @@ export class PauseResumeManager { updatedAt: now, }, }) + + await this.processQueuedResumes(executionId) } -} + static async enqueueOrStartResume(args: EnqueueResumeArgs): Promise { + const { executionId, contextId, resumeInput, userId } = args + + return await db.transaction(async (tx) => { + const pausedExecution = await tx + .select() + .from(pausedExecutions) + .where(eq(pausedExecutions.executionId, executionId)) + .limit(1) + .then((rows) => rows[0]) + + if (!pausedExecution) { + throw new Error('Paused execution not found or already resumed') + } + + const pausePoints = pausedExecution.pausePoints as Record + const pausePoint = pausePoints?.[contextId] + if (!pausePoint) { + throw new Error('Pause point not found for execution') + } + if (pausePoint.resumeStatus !== 'paused') { + throw new Error('Pause point already resumed') + } + + const activeResume = await tx + .select({ id: resumeQueue.id }) + .from(resumeQueue) + .where( + and( + eq(resumeQueue.parentExecutionId, executionId), + inArray(resumeQueue.status, ['claimed'] as const) + ) + ) + .limit(1) + .then((rows) => rows[0]) + + const resumeExecutionId = uuidv4() + const now = new Date() + + if (activeResume) { + const [entry] = await tx + .insert(resumeQueue) + .values({ + id: uuidv4(), + pausedExecutionId: pausedExecution.id, + parentExecutionId: executionId, + newExecutionId: resumeExecutionId, + contextId, + resumeInput: resumeInput ?? null, + status: 'pending', + queuedAt: now, + }) + .returning({ id: resumeQueue.id, queuedAt: resumeQueue.queuedAt }) + + const [{ position }] = await tx + .select({ position: sql`count(*)` }) + .from(resumeQueue) + .where( + and( + eq(resumeQueue.parentExecutionId, executionId), + eq(resumeQueue.status, 'pending'), + lt(resumeQueue.queuedAt, entry.queuedAt) + ) + ) + + return { + status: 'queued', + resumeExecutionId, + queuePosition: Number(position) + 1, + } + } + + const resumeEntryId = uuidv4() + await tx.insert(resumeQueue).values({ + id: resumeEntryId, + pausedExecutionId: pausedExecution.id, + parentExecutionId: executionId, + newExecutionId: resumeExecutionId, + contextId, + resumeInput: resumeInput ?? null, + status: 'claimed', + queuedAt: now, + claimedAt: now, + }) + + return { + status: 'starting', + resumeExecutionId, + resumeEntryId, + pausedExecution, + contextId, + resumeInput, + userId, + } + }) + } + + static async startResumeExecution(args: StartResumeExecutionArgs): Promise { + const { resumeEntryId, resumeExecutionId, pausedExecution, contextId, resumeInput, userId } = + args + + try { + await this.runResumeExecution({ + resumeExecutionId, + pausedExecution, + contextId, + resumeInput, + userId, + }) + + await this.markResumeCompleted({ + resumeEntryId, + pausedExecutionId: pausedExecution.id, + parentExecutionId: pausedExecution.executionId, + contextId, + }) + + await this.processQueuedResumes(pausedExecution.executionId) + } catch (error) { + await this.markResumeFailed({ resumeEntryId, failureReason: (error as Error).message }) + throw error + } + } + + private static async runResumeExecution(args: { + resumeExecutionId: string + pausedExecution: typeof pausedExecutions.$inferSelect + contextId: string + resumeInput: any + userId: string + }): Promise { + const { resumeExecutionId, pausedExecution, contextId, resumeInput, userId } = args + + const serializedSnapshot = pausedExecution.executionSnapshot as SerializedSnapshot + const baseSnapshot = ExecutionSnapshot.fromJSON(serializedSnapshot.snapshot) + const pausePoints = pausedExecution.pausePoints as Record + const pausePoint = pausePoints?.[contextId] + if (!pausePoint) { + throw new Error('Pause point not found for resume execution') + } + + const triggerBlockId: string = pausePoint.triggerBlockId + + const stateCopy = baseSnapshot.state + ? { + ...baseSnapshot.state, + blockStates: { ...baseSnapshot.state.blockStates }, + } + : undefined + + if (stateCopy) { + stateCopy.pendingQueue = [triggerBlockId] + const triggerState = stateCopy.blockStates[triggerBlockId] ?? { + output: {}, + executed: true, + executionTime: 0, + } + triggerState.output = { + ...triggerState.output, + input: resumeInput ?? {}, + resumedFrom: pausedExecution.executionId, + } + triggerState.executed = true + triggerState.executionTime = 0 + stateCopy.blockStates[triggerBlockId] = triggerState + } + + const metadata = { + ...baseSnapshot.metadata, + executionId: resumeExecutionId, + requestId: resumeExecutionId.slice(0, 8), + triggerBlockId, + startTime: new Date().toISOString(), + userId, + } + + const resumeSnapshot = new ExecutionSnapshot( + metadata, + baseSnapshot.workflow, + resumeInput ?? {}, + {}, + baseSnapshot.workflowVariables || {}, + baseSnapshot.selectedOutputs || [], + stateCopy + ) + + const triggerType = (metadata.triggerType as + | 'api' + | 'webhook' + | 'schedule' + | 'manual' + | 'chat' + | undefined) ?? 'manual' + const loggingSession = new LoggingSession( + metadata.workflowId, + resumeExecutionId, + triggerType, + metadata.requestId + ) + + await executeWorkflowCore({ + snapshot: resumeSnapshot, + callbacks: {}, + loggingSession, + }) + } + + private static async markResumeCompleted(args: { + resumeEntryId: string + pausedExecutionId: string + parentExecutionId: string + contextId: string + }): Promise { + const { resumeEntryId, pausedExecutionId, parentExecutionId, contextId } = args + const now = new Date() + + await db.transaction(async (tx) => { + await tx + .update(resumeQueue) + .set({ status: 'completed', completedAt: now, failureReason: null }) + .where(eq(resumeQueue.id, resumeEntryId)) + + await tx + .update(pausedExecutions) + .set({ + pausePoints: sql`jsonb_set(jsonb_set(pause_points, ARRAY[${contextId}, 'resumeStatus'], '"resumed"'::jsonb), ARRAY[${contextId}, 'resumedAt'], to_jsonb(${now.toISOString()}))`, + resumedCount: sql`resumed_count + 1`, + status: sql`CASE WHEN resumed_count + 1 >= total_pause_count THEN 'fully_resumed' ELSE 'partially_resumed' END`, + updatedAt: now, + }) + .where(eq(pausedExecutions.id, pausedExecutionId)) + + const [{ remaining }] = await tx + .select({ remaining: sql`total_pause_count - resumed_count - 1` }) + .from(pausedExecutions) + .where(eq(pausedExecutions.executionId, parentExecutionId)) + + if (Number(remaining) <= 0) { + await tx + .update(pausedExecutions) + .set({ status: 'fully_resumed', updatedAt: now }) + .where(eq(pausedExecutions.executionId, parentExecutionId)) + } + }) + } + + private static async markResumeFailed(args: { + resumeEntryId: string + failureReason: string + }): Promise { + await db + .update(resumeQueue) + .set({ status: 'failed', failureReason: args.failureReason, completedAt: new Date() }) + .where(eq(resumeQueue.id, args.resumeEntryId)) + } + + static async processQueuedResumes(parentExecutionId: string): Promise { + const pendingEntry = await db.transaction(async (tx) => { + const entry = await tx + .select() + .from(resumeQueue) + .where(and(eq(resumeQueue.parentExecutionId, parentExecutionId), eq(resumeQueue.status, 'pending'))) + .orderBy(resumeQueue.queuedAt) + .limit(1) + .then((rows) => rows[0]) + + if (!entry) { + return null + } + + await tx + .update(resumeQueue) + .set({ status: 'claimed', claimedAt: new Date() }) + .where(eq(resumeQueue.id, entry.id)) + + const pausedExecution = await tx + .select() + .from(pausedExecutions) + .where(eq(pausedExecutions.id, entry.pausedExecutionId)) + .limit(1) + .then((rows) => rows[0]) + + if (!pausedExecution) { + return null + } + + return { entry, pausedExecution } + }) + + if (!pendingEntry) { + return + } + + const { entry, pausedExecution } = pendingEntry + + void this.startResumeExecution({ + resumeEntryId: entry.id, + resumeExecutionId: entry.newExecutionId, + pausedExecution, + contextId: entry.contextId, + resumeInput: entry.resumeInput, + userId: '', + }) + } +} From a7c3deaa98729f088664fcf098aed5d2793f0094 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 5 Nov 2025 13:01:00 -0800 Subject: [PATCH 08/37] Ui checkpoint --- .../[executionId]/[contextId]/route.ts | 116 +++++ .../app/api/workflows/[id]/execute/route.ts | 6 + .../[id]/paused/[executionId]/route.ts | 34 ++ .../app/api/workflows/[id]/paused/route.ts | 31 ++ .../[executionId]/[contextId]/page.tsx | 32 ++ .../[contextId]/resume-page-client.tsx | 456 ++++++++++++++++++ apps/sim/background/schedule-execution.ts | 3 + apps/sim/background/webhook-execution.ts | 6 + apps/sim/background/workflow-execution.ts | 3 + apps/sim/executor/execution/engine.ts | 1 + .../pause-resume/pause-resume-handler.test.ts | 36 +- .../pause-resume/pause-resume-handler.ts | 26 +- apps/sim/executor/types.ts | 16 + .../executor/pause-resume-manager.ts | 318 +++++++++++- 14 files changed, 1070 insertions(+), 14 deletions(-) create mode 100644 apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts create mode 100644 apps/sim/app/api/workflows/[id]/paused/[executionId]/route.ts create mode 100644 apps/sim/app/api/workflows/[id]/paused/route.ts create mode 100644 apps/sim/app/resume/[workflowId]/[executionId]/[contextId]/page.tsx create mode 100644 apps/sim/app/resume/[workflowId]/[executionId]/[contextId]/resume-page-client.tsx diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts new file mode 100644 index 0000000000..6b98e6db19 --- /dev/null +++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts @@ -0,0 +1,116 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createLogger } from '@/lib/logs/console/logger' +import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager' +import { validateWorkflowAccess } from '@/app/api/workflows/middleware' + +const logger = createLogger('WorkflowResumeAPI') + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function POST( + request: NextRequest, + { + params, + }: { + params: { workflowId: string; executionId: string; contextId: string } + } +) { + const { workflowId, executionId, contextId } = params + + const access = await validateWorkflowAccess(request, workflowId, false) + if (access.error) { + return NextResponse.json({ error: access.error.message }, { status: access.error.status }) + } + + const workflow = access.workflow! + + let payload: any = {} + try { + payload = await request.json() + } catch { + payload = {} + } + + const resumeInput = payload?.input ?? payload ?? {} + const userId = workflow.userId ?? '' + + try { + const enqueueResult = await PauseResumeManager.enqueueOrStartResume({ + executionId, + contextId, + resumeInput, + userId, + }) + + if (enqueueResult.status === 'queued') { + return NextResponse.json({ + status: 'queued', + executionId: enqueueResult.resumeExecutionId, + queuePosition: enqueueResult.queuePosition, + message: 'Resume queued. It will run after current resumes finish.', + }) + } + + PauseResumeManager.startResumeExecution({ + resumeEntryId: enqueueResult.resumeEntryId, + resumeExecutionId: enqueueResult.resumeExecutionId, + pausedExecution: enqueueResult.pausedExecution, + contextId: enqueueResult.contextId, + resumeInput: enqueueResult.resumeInput, + userId: enqueueResult.userId, + }).catch((error) => { + logger.error('Failed to start resume execution', { + workflowId, + parentExecutionId: executionId, + resumeExecutionId: enqueueResult.resumeExecutionId, + error + }) + }) + + return NextResponse.json({ + status: 'started', + executionId: enqueueResult.resumeExecutionId, + message: 'Resume execution started.', + }) + } catch (error: any) { + logger.error('Resume request failed', { + workflowId, + executionId, + contextId, + error, + }) + return NextResponse.json( + { error: error.message || 'Failed to queue resume request' }, + { status: 400 } + ) + } +} + +export async function GET( + request: NextRequest, + { + params, + }: { + params: { workflowId: string; executionId: string; contextId: string } + } +) { + const { workflowId, executionId, contextId } = params + + const access = await validateWorkflowAccess(request, workflowId, false) + if (access.error) { + return NextResponse.json({ error: access.error.message }, { status: access.error.status }) + } + + const detail = await PauseResumeManager.getPauseContextDetail({ + workflowId, + executionId, + contextId, + }) + + if (!detail) { + return NextResponse.json({ error: 'Pause context not found' }, { status: 404 }) + } + + return NextResponse.json(detail) +} diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 3ebba186c2..70870b4737 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -141,8 +141,11 @@ export async function executeWorkflow( executionId, pausePoints: result.pausePoints || [], snapshotSeed: result.snapshotSeed, + executorUserId: result.metadata?.userId, }) } + } else { + await PauseResumeManager.processQueuedResumes(executionId) } if (streamConfig?.skipLoggingComplete) { @@ -571,8 +574,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: executionId, pausePoints: result.pausePoints || [], snapshotSeed: result.snapshotSeed, + executorUserId: result.metadata?.userId, }) } + } else { + await PauseResumeManager.processQueuedResumes(executionId) } if (result.error === 'Workflow execution was cancelled') { diff --git a/apps/sim/app/api/workflows/[id]/paused/[executionId]/route.ts b/apps/sim/app/api/workflows/[id]/paused/[executionId]/route.ts new file mode 100644 index 0000000000..52c638cf30 --- /dev/null +++ b/apps/sim/app/api/workflows/[id]/paused/[executionId]/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server' +import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager' +import { validateWorkflowAccess } from '@/app/api/workflows/middleware' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function GET( + request: NextRequest, + { + params, + }: { + params: { id: string; executionId: string } + } +) { + const workflowId = params.id + const executionId = params.executionId + + const access = await validateWorkflowAccess(request, workflowId, false) + if (access.error) { + return NextResponse.json({ error: access.error.message }, { status: access.error.status }) + } + + const detail = await PauseResumeManager.getPausedExecutionDetail({ + workflowId, + executionId, + }) + + if (!detail) { + return NextResponse.json({ error: 'Paused execution not found' }, { status: 404 }) + } + + return NextResponse.json(detail) +} diff --git a/apps/sim/app/api/workflows/[id]/paused/route.ts b/apps/sim/app/api/workflows/[id]/paused/route.ts new file mode 100644 index 0000000000..d8e5378232 --- /dev/null +++ b/apps/sim/app/api/workflows/[id]/paused/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from 'next/server' +import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager' +import { validateWorkflowAccess } from '@/app/api/workflows/middleware' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function GET( + request: NextRequest, + { + params, + }: { + params: { id: string } + } +) { + const workflowId = params.id + + const access = await validateWorkflowAccess(request, workflowId, false) + if (access.error) { + return NextResponse.json({ error: access.error.message }, { status: access.error.status }) + } + + const statusFilter = request.nextUrl.searchParams.get('status') || undefined + + const pausedExecutions = await PauseResumeManager.listPausedExecutions({ + workflowId, + status: statusFilter, + }) + + return NextResponse.json({ pausedExecutions }) +} diff --git a/apps/sim/app/resume/[workflowId]/[executionId]/[contextId]/page.tsx b/apps/sim/app/resume/[workflowId]/[executionId]/[contextId]/page.tsx new file mode 100644 index 0000000000..56ad2e8e28 --- /dev/null +++ b/apps/sim/app/resume/[workflowId]/[executionId]/[contextId]/page.tsx @@ -0,0 +1,32 @@ +import { PauseResumeManager } from '@/lib/workflows/executor/pause-resume-manager' +import ResumeClientPage from './resume-page-client' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +interface PageParams { + workflowId: string + executionId: string + contextId: string +} + +export default async function ResumePage({ + params, +}: { + params: PageParams +}) { + const { workflowId, executionId, contextId } = params + + const detail = await PauseResumeManager.getPauseContextDetail({ + workflowId, + executionId, + contextId, + }) + + return ( + + ) +} diff --git a/apps/sim/app/resume/[workflowId]/[executionId]/[contextId]/resume-page-client.tsx b/apps/sim/app/resume/[workflowId]/[executionId]/[contextId]/resume-page-client.tsx new file mode 100644 index 0000000000..7735887164 --- /dev/null +++ b/apps/sim/app/resume/[workflowId]/[executionId]/[contextId]/resume-page-client.tsx @@ -0,0 +1,456 @@ +'use client' + +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button' +import { Textarea } from '@/components/ui/textarea' +import Nav from '@/app/(landing)/components/nav/nav' +import { inter } from '@/app/fonts/inter' +import { soehne } from '@/app/fonts/soehne/soehne' +import { useBrandConfig } from '@/lib/branding/branding' + +interface ResumeLinks { + apiUrl: string + uiUrl: string + contextId: string + executionId: string + workflowId: string +} + +interface ResumeQueueEntrySummary { + id: string + contextId: string + status: string + queuedAt: string | null + claimedAt: string | null + completedAt: string | null + failureReason: string | null + newExecutionId: string + resumeInput: any +} + +interface PausePointWithQueue { + contextId: string + triggerBlockId: string + response: any + registeredAt: string + resumeStatus: 'paused' | 'resumed' | 'failed' | 'queued' | 'resuming' + snapshotReady: boolean + resumeLinks?: ResumeLinks + queuePosition?: number | null + latestResumeEntry?: ResumeQueueEntrySummary | null +} + +interface PausedExecutionSummary { + id: string + workflowId: string + executionId: string + status: string + totalPauseCount: number + resumedCount: number + pausedAt: string | null + updatedAt: string | null + expiresAt: string | null + metadata: Record | null + triggerIds: string[] + pausePoints: PausePointWithQueue[] +} + +interface PauseContextDetail { + execution: PausedExecutionSummary + pausePoint: PausePointWithQueue + queue: ResumeQueueEntrySummary[] + activeResumeEntry?: ResumeQueueEntrySummary | null +} + +interface ResumeClientProps { + params: { workflowId: string; executionId: string; contextId: string } + initialDetail: PauseContextDetail | null +} + +const POLL_INTERVAL_MS = 5000 + +function formatDate(value: string | null): string { + if (!value) return '—' + try { + return new Date(value).toLocaleString() + } catch { + return value + } +} + +export default function ResumeClientPage({ params, initialDetail }: ResumeClientProps) { + const { workflowId, executionId, contextId } = params + const router = useRouter() + const brandConfig = useBrandConfig() + + const [detail, setDetail] = useState(initialDetail) + const [status, setStatus] = useState(initialDetail?.pausePoint.resumeStatus ?? 'paused') + const [queuePosition, setQueuePosition] = useState( + initialDetail?.pausePoint.queuePosition + ) + const [buttonClass, setButtonClass] = useState('auth-button-gradient') + const [resumeInput, setResumeInput] = useState(() => + initialDetail?.pausePoint.response?.data + ? JSON.stringify(initialDetail.pausePoint.response.data, null, 2) + : '' + ) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [message, setMessage] = useState(null) + + const pauseLinks = detail?.pausePoint.resumeLinks + const resumeDisabled = + loading || status === 'resumed' || status === 'failed' || status === 'resuming' + + useEffect(() => { + const root = document.documentElement + const hadDark = root.classList.contains('dark') + const hadLight = root.classList.contains('light') + root.classList.add('light') + root.classList.remove('dark') + return () => { + if (!hadLight) root.classList.remove('light') + if (hadDark) root.classList.add('dark') + } + }, []) + + useEffect(() => { + const checkCustomBrand = () => { + const computedStyle = getComputedStyle(document.documentElement) + const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim() + if (brandAccent && brandAccent !== '#6f3dfa') { + setButtonClass('auth-button-custom') + } else { + setButtonClass('auth-button-gradient') + } + } + checkCustomBrand() + window.addEventListener('resize', checkCustomBrand) + const observer = new MutationObserver(checkCustomBrand) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['style', 'class'], + }) + return () => { + window.removeEventListener('resize', checkCustomBrand) + observer.disconnect() + } + }, []) + + useEffect(() => { + setDetail(initialDetail) + setStatus(initialDetail?.pausePoint.resumeStatus ?? 'paused') + setQueuePosition(initialDetail?.pausePoint.queuePosition) + }, [initialDetail]) + + const refreshDetail = useCallback(async () => { + try { + const response = await fetch( + `/api/resume/${workflowId}/${executionId}/${contextId}`, + { + method: 'GET', + credentials: 'include', + cache: 'no-store', + } + ) + + if (!response.ok) { + return + } + + const data: PauseContextDetail = await response.json() + setDetail(data) + setStatus(data.pausePoint.resumeStatus) + setQueuePosition(data.pausePoint.queuePosition) + } catch (err) { + console.error('Failed to refresh pause context', err) + } + }, [workflowId, executionId, contextId]) + + useEffect(() => { + if (!detail) return + if (status === 'resumed' || status === 'failed') { + return + } + + const interval = window.setInterval(() => { + refreshDetail() + }, POLL_INTERVAL_MS) + + return () => window.clearInterval(interval) + }, [detail, status, refreshDetail]) + + const handleResume = useCallback(async () => { + setLoading(true) + setError(null) + setMessage(null) + + let parsedInput: any = undefined + + if (resumeInput && resumeInput.trim().length > 0) { + try { + parsedInput = JSON.parse(resumeInput) + } catch (err: any) { + setError('Resume input must be valid JSON.') + setLoading(false) + return + } + } + + try { + const response = await fetch(`/api/resume/${workflowId}/${executionId}/${contextId}`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(parsedInput ? { input: parsedInput } : {}), + }) + + const payload = await response.json() + + if (!response.ok) { + setError(payload.error || 'Failed to resume execution.') + setStatus(detail?.pausePoint.resumeStatus ?? 'paused') + return + } + + if (payload.status === 'queued') { + setStatus('queued') + setQueuePosition(payload.queuePosition) + setMessage('Resume request queued. This page will refresh automatically.') + } else { + setStatus('resuming') + setMessage('Resume execution started. Monitoring for completion...') + } + + await refreshDetail() + } catch (err: any) { + setError(err.message || 'Unexpected error while resuming execution.') + } finally { + setLoading(false) + } + }, [resumeInput, workflowId, executionId, contextId, detail, refreshDetail]) + + const statusLabel = useMemo(() => { + if (status === 'queued') { + if (queuePosition && queuePosition > 0) { + return `Queued (position ${queuePosition})` + } + return 'Queued' + } + + if (status === 'resuming') { + return 'Resuming' + } + + return status.charAt(0).toUpperCase() + status.slice(1) + }, [status, queuePosition]) + + if (!detail) { + return ( +
+
+ ) + } + + const pauseResponsePreview = useMemo(() => { + try { + return JSON.stringify(detail.pausePoint.response?.data ?? {}, null, 2) + } catch { + return String(detail.pausePoint.response?.data ?? '') + } + }, [detail.pausePoint.response]) + + return ( +
+