Skip to content

Commit ff768ca

Browse files
Sg312icecrasher321
andauthored
fix(copilot): fix webhook triggers unsaving in new diff store (#2096)
* Fix copilot trigger unsave * Fix for schedules * fix lint * hide schedule save subblock from preview --------- Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
1 parent bbaf7e9 commit ff768ca

File tree

4 files changed

+417
-87
lines changed

4 files changed

+417
-87
lines changed

apps/sim/app/api/workflows/[id]/state/route.ts

Lines changed: 311 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import { db } from '@sim/db'
2-
import { workflow } from '@sim/db/schema'
2+
import { webhook, workflow, workflowSchedule } from '@sim/db/schema'
33
import { eq } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { z } from 'zod'
66
import { getSession } from '@/lib/auth'
77
import { env } from '@/lib/env'
88
import { createLogger } from '@/lib/logs/console/logger'
9+
import {
10+
calculateNextRunTime,
11+
generateCronExpression,
12+
getScheduleTimeValues,
13+
validateCronExpression,
14+
} from '@/lib/schedules/utils'
915
import { generateRequestId } from '@/lib/utils'
1016
import { extractAndPersistCustomTools } from '@/lib/workflows/custom-tools-persistence'
1117
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
1218
import { getWorkflowAccessContext } from '@/lib/workflows/utils'
1319
import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation'
1420
import type { BlockState } from '@/stores/workflows/workflow/types'
1521
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
22+
import { getTrigger } from '@/triggers'
1623

1724
const logger = createLogger('WorkflowStateAPI')
1825

@@ -202,6 +209,9 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
202209
)
203210
}
204211

212+
await syncWorkflowWebhooks(workflowId, workflowState.blocks)
213+
await syncWorkflowSchedules(workflowId, workflowState.blocks)
214+
205215
// Extract and persist custom tools to database
206216
try {
207217
const workspaceId = workflowData.workspaceId
@@ -287,3 +297,303 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
287297
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
288298
}
289299
}
300+
301+
function getSubBlockValue<T = unknown>(block: BlockState, subBlockId: string): T | undefined {
302+
const value = block.subBlocks?.[subBlockId]?.value
303+
if (value === undefined || value === null) {
304+
return undefined
305+
}
306+
return value as T
307+
}
308+
309+
async function syncWorkflowWebhooks(
310+
workflowId: string,
311+
blocks: Record<string, any>
312+
): Promise<void> {
313+
await syncBlockResources(workflowId, blocks, {
314+
resourceName: 'webhook',
315+
subBlockId: 'webhookId',
316+
buildMetadata: buildWebhookMetadata,
317+
applyMetadata: upsertWebhookRecord,
318+
})
319+
}
320+
321+
type ScheduleBlockInput = Parameters<typeof getScheduleTimeValues>[0]
322+
323+
async function syncWorkflowSchedules(
324+
workflowId: string,
325+
blocks: Record<string, any>
326+
): Promise<void> {
327+
await syncBlockResources(workflowId, blocks, {
328+
resourceName: 'schedule',
329+
subBlockId: 'scheduleId',
330+
buildMetadata: buildScheduleMetadata,
331+
applyMetadata: upsertScheduleRecord,
332+
})
333+
}
334+
335+
interface ScheduleMetadata {
336+
cronExpression: string | null
337+
nextRunAt: Date | null
338+
timezone: string
339+
}
340+
341+
function buildScheduleMetadata(block: BlockState): ScheduleMetadata | null {
342+
const scheduleType = getSubBlockValue<string>(block, 'scheduleType') || 'daily'
343+
const scheduleBlock = convertToScheduleBlock(block)
344+
345+
const scheduleValues = getScheduleTimeValues(scheduleBlock)
346+
const sanitizedValues =
347+
scheduleType !== 'custom' ? { ...scheduleValues, cronExpression: null } : scheduleValues
348+
349+
try {
350+
const cronExpression = generateCronExpression(scheduleType, sanitizedValues)
351+
const timezone = scheduleValues.timezone || 'UTC'
352+
353+
if (cronExpression) {
354+
const validation = validateCronExpression(cronExpression, timezone)
355+
if (!validation.isValid) {
356+
logger.warn('Invalid cron expression while syncing schedule', {
357+
blockId: block.id,
358+
cronExpression,
359+
error: validation.error,
360+
})
361+
return null
362+
}
363+
}
364+
365+
const nextRunAt = calculateNextRunTime(scheduleType, sanitizedValues)
366+
367+
return {
368+
cronExpression,
369+
timezone,
370+
nextRunAt,
371+
}
372+
} catch (error) {
373+
logger.error('Failed to build schedule metadata during sync', {
374+
blockId: block.id,
375+
error,
376+
})
377+
return null
378+
}
379+
}
380+
381+
function convertToScheduleBlock(block: BlockState): ScheduleBlockInput {
382+
const subBlocks: ScheduleBlockInput['subBlocks'] = {}
383+
384+
Object.entries(block.subBlocks || {}).forEach(([id, subBlock]) => {
385+
subBlocks[id] = { value: stringifySubBlockValue(subBlock?.value) }
386+
})
387+
388+
return {
389+
type: block.type,
390+
subBlocks,
391+
}
392+
}
393+
394+
interface WebhookMetadata {
395+
triggerPath: string
396+
provider: string | null
397+
providerConfig: Record<string, any>
398+
}
399+
400+
function buildWebhookMetadata(block: BlockState): WebhookMetadata | null {
401+
const triggerId =
402+
getSubBlockValue<string>(block, 'triggerId') ||
403+
getSubBlockValue<string>(block, 'selectedTriggerId')
404+
const triggerConfig = getSubBlockValue<Record<string, any>>(block, 'triggerConfig') || {}
405+
const triggerCredentials = getSubBlockValue<string>(block, 'triggerCredentials')
406+
const triggerPath = getSubBlockValue<string>(block, 'triggerPath') || block.id
407+
408+
const triggerDef = triggerId ? getTrigger(triggerId) : undefined
409+
const provider = triggerDef?.provider || null
410+
411+
const providerConfig = {
412+
...(typeof triggerConfig === 'object' ? triggerConfig : {}),
413+
...(triggerCredentials ? { credentialId: triggerCredentials } : {}),
414+
...(triggerId ? { triggerId } : {}),
415+
}
416+
417+
return {
418+
triggerPath,
419+
provider,
420+
providerConfig,
421+
}
422+
}
423+
424+
async function upsertWebhookRecord(
425+
workflowId: string,
426+
block: BlockState,
427+
webhookId: string,
428+
metadata: WebhookMetadata
429+
): Promise<void> {
430+
const [existing] = await db.select().from(webhook).where(eq(webhook.id, webhookId)).limit(1)
431+
432+
if (existing) {
433+
const needsUpdate =
434+
existing.blockId !== block.id ||
435+
existing.workflowId !== workflowId ||
436+
existing.path !== metadata.triggerPath
437+
438+
if (needsUpdate) {
439+
await db
440+
.update(webhook)
441+
.set({
442+
workflowId,
443+
blockId: block.id,
444+
path: metadata.triggerPath,
445+
provider: metadata.provider || existing.provider,
446+
providerConfig: Object.keys(metadata.providerConfig).length
447+
? metadata.providerConfig
448+
: existing.providerConfig,
449+
isActive: true,
450+
updatedAt: new Date(),
451+
})
452+
.where(eq(webhook.id, webhookId))
453+
}
454+
return
455+
}
456+
457+
await db.insert(webhook).values({
458+
id: webhookId,
459+
workflowId,
460+
blockId: block.id,
461+
path: metadata.triggerPath,
462+
provider: metadata.provider,
463+
providerConfig: metadata.providerConfig,
464+
isActive: true,
465+
createdAt: new Date(),
466+
updatedAt: new Date(),
467+
})
468+
469+
logger.info('Recreated missing webhook after workflow save', {
470+
workflowId,
471+
blockId: block.id,
472+
webhookId,
473+
})
474+
}
475+
476+
async function upsertScheduleRecord(
477+
workflowId: string,
478+
block: BlockState,
479+
scheduleId: string,
480+
metadata: ScheduleMetadata
481+
): Promise<void> {
482+
const now = new Date()
483+
const [existing] = await db
484+
.select({
485+
id: workflowSchedule.id,
486+
nextRunAt: workflowSchedule.nextRunAt,
487+
})
488+
.from(workflowSchedule)
489+
.where(eq(workflowSchedule.id, scheduleId))
490+
.limit(1)
491+
492+
if (existing) {
493+
await db
494+
.update(workflowSchedule)
495+
.set({
496+
workflowId,
497+
blockId: block.id,
498+
cronExpression: metadata.cronExpression,
499+
nextRunAt: metadata.nextRunAt ?? existing.nextRunAt,
500+
timezone: metadata.timezone,
501+
updatedAt: now,
502+
})
503+
.where(eq(workflowSchedule.id, scheduleId))
504+
return
505+
}
506+
507+
await db.insert(workflowSchedule).values({
508+
id: scheduleId,
509+
workflowId,
510+
blockId: block.id,
511+
cronExpression: metadata.cronExpression,
512+
nextRunAt: metadata.nextRunAt ?? null,
513+
triggerType: 'schedule',
514+
timezone: metadata.timezone,
515+
status: 'active',
516+
failedCount: 0,
517+
createdAt: now,
518+
updatedAt: now,
519+
})
520+
521+
logger.info('Recreated missing schedule after workflow save', {
522+
workflowId,
523+
blockId: block.id,
524+
scheduleId,
525+
})
526+
}
527+
528+
interface BlockResourceSyncConfig<T> {
529+
resourceName: string
530+
subBlockId: string
531+
buildMetadata: (block: BlockState, resourceId: string) => T | null
532+
applyMetadata: (
533+
workflowId: string,
534+
block: BlockState,
535+
resourceId: string,
536+
metadata: T
537+
) => Promise<void>
538+
}
539+
540+
async function syncBlockResources<T>(
541+
workflowId: string,
542+
blocks: Record<string, any>,
543+
config: BlockResourceSyncConfig<T>
544+
): Promise<void> {
545+
const blockEntries = Object.values(blocks || {}).filter(Boolean) as BlockState[]
546+
if (blockEntries.length === 0) return
547+
548+
for (const block of blockEntries) {
549+
const resourceId = getSubBlockValue<string>(block, config.subBlockId)
550+
if (!resourceId) continue
551+
552+
const metadata = config.buildMetadata(block, resourceId)
553+
if (!metadata) {
554+
logger.warn(`Skipping ${config.resourceName} sync due to invalid configuration`, {
555+
workflowId,
556+
blockId: block.id,
557+
resourceId,
558+
resourceName: config.resourceName,
559+
})
560+
continue
561+
}
562+
563+
try {
564+
await config.applyMetadata(workflowId, block, resourceId, metadata)
565+
} catch (error) {
566+
logger.error(`Failed to sync ${config.resourceName}`, {
567+
workflowId,
568+
blockId: block.id,
569+
resourceId,
570+
resourceName: config.resourceName,
571+
error,
572+
})
573+
}
574+
}
575+
}
576+
577+
function stringifySubBlockValue(value: unknown): string {
578+
if (value === undefined || value === null) {
579+
return ''
580+
}
581+
582+
if (typeof value === 'string') {
583+
return value
584+
}
585+
586+
if (typeof value === 'number' || typeof value === 'boolean') {
587+
return String(value)
588+
}
589+
590+
if (value instanceof Date) {
591+
return value.toISOString()
592+
}
593+
594+
try {
595+
return JSON.stringify(value)
596+
} catch {
597+
return String(value)
598+
}
599+
}

apps/sim/blocks/blocks/schedule.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ export const ScheduleBlock: BlockConfig = {
159159
id: 'scheduleSave',
160160
type: 'schedule-save',
161161
mode: 'trigger',
162+
hideFromPreview: true,
162163
},
163164

164165
{

0 commit comments

Comments
 (0)